nfcscript 0.0.16__tar.gz → 0.0.17__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.16 → nfcscript-0.0.17}/PKG-INFO +1 -1
- {nfcscript-0.0.16 → nfcscript-0.0.17}/pyproject.toml +2 -5
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/nfc_cli.py +3 -3
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/reader.py +197 -64
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/PKG-INFO +1 -1
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_reader.py +139 -83
- {nfcscript-0.0.16 → nfcscript-0.0.17}/README.md +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/setup.cfg +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/__init__.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/assertions.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/bits.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/checksum.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/cli.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/delay.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/hex_util.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfc/trace.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/SOURCES.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/dependency_links.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/entry_points.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/requires.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/src/nfcscript.egg-info/top_level.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_assertions.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_bits.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_checksum.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_cli.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_cli_collect.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_delay.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_hex_util.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.17}/tests/test_trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nfcscript"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.17"
|
|
4
4
|
description = "A plugin-based core engine for DSL-style NFC testing automation."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -24,9 +24,6 @@ testpaths = ["tests"]
|
|
|
24
24
|
[tool.uv]
|
|
25
25
|
package = true
|
|
26
26
|
|
|
27
|
-
[tool.uv.sources]
|
|
28
|
-
nfctester = { workspace = true }
|
|
29
|
-
|
|
30
27
|
[dependency-groups]
|
|
31
28
|
dev = [
|
|
32
29
|
"pytest",
|
|
@@ -34,7 +31,7 @@ dev = [
|
|
|
34
31
|
]
|
|
35
32
|
|
|
36
33
|
[tool.bumpversion]
|
|
37
|
-
current_version = "0.0.
|
|
34
|
+
current_version = "0.0.17"
|
|
38
35
|
commit = true
|
|
39
36
|
tag = true
|
|
40
37
|
allow_dirty = true
|
|
@@ -172,13 +172,13 @@ def main():
|
|
|
172
172
|
parser.error("需要指定卡片类型 (-c/--card) 或使用 --list-cards 查看可用类型")
|
|
173
173
|
|
|
174
174
|
try:
|
|
175
|
-
connect(port=args.port, reader_type=args.reader)
|
|
175
|
+
reader.connect(port=args.port, reader_type=args.reader)
|
|
176
176
|
except Exception as e:
|
|
177
177
|
print(f"连接读卡器失败: {e}")
|
|
178
178
|
sys.exit(1)
|
|
179
179
|
|
|
180
180
|
try:
|
|
181
|
-
card = CardRegistry.create(args.card, reader=
|
|
181
|
+
card = CardRegistry.create(args.card, reader=reader.target)
|
|
182
182
|
methods = _get_methods(card)
|
|
183
183
|
|
|
184
184
|
print(f"已连接卡片: {args.card}")
|
|
@@ -220,7 +220,7 @@ def main():
|
|
|
220
220
|
print(f"错误: {e}")
|
|
221
221
|
|
|
222
222
|
finally:
|
|
223
|
-
close()
|
|
223
|
+
reader.close()
|
|
224
224
|
print("已断开连接")
|
|
225
225
|
|
|
226
226
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import atexit
|
|
1
2
|
import os
|
|
3
|
+
import sys
|
|
2
4
|
from contextlib import contextmanager
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
3
8
|
from nfctester.drivers.card_reader import CardInfo, TransceiveBits
|
|
4
9
|
from nfctester.registry import CardReaderRegistry
|
|
5
10
|
from nfctester.trace import trace as _trace
|
|
@@ -7,44 +12,104 @@ from .assertions import ASSERT_EQUAL, ASSERT_IS_NOT_NONE, ASSERT_LEN
|
|
|
7
12
|
from .checksum import GET_BCC
|
|
8
13
|
from .hex_util import FORMAT_HEX
|
|
9
14
|
|
|
15
|
+
_DOTENV_LOADED = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _find_dotenv_all(start_dir: str) -> list[str]:
|
|
19
|
+
"""从起始目录逐级向上搜索所有 .env 文件"""
|
|
20
|
+
chain = []
|
|
21
|
+
path = start_dir
|
|
22
|
+
while True:
|
|
23
|
+
env_path = os.path.join(path, ".env")
|
|
24
|
+
if os.path.exists(env_path):
|
|
25
|
+
chain.append(env_path)
|
|
26
|
+
parent = os.path.dirname(path)
|
|
27
|
+
if parent == path:
|
|
28
|
+
break
|
|
29
|
+
path = parent
|
|
30
|
+
chain.reverse()
|
|
31
|
+
return chain
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_env_configs() -> None:
|
|
35
|
+
"""按需加载当前目录及上级目录的 .env 文件"""
|
|
36
|
+
global _DOTENV_LOADED
|
|
37
|
+
if not _DOTENV_LOADED:
|
|
38
|
+
try:
|
|
39
|
+
cwd = os.getcwd()
|
|
40
|
+
for env_path in _find_dotenv_all(cwd):
|
|
41
|
+
load_dotenv(env_path, override=False)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
_DOTENV_LOADED = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ─── 轻量读卡器门面单例 ───────────────────────────────────────────────────
|
|
10
48
|
|
|
11
|
-
class
|
|
12
|
-
"""
|
|
49
|
+
class _ReaderFacade:
|
|
50
|
+
"""
|
|
51
|
+
读卡器单例门面对象 (`reader`)。
|
|
52
|
+
|
|
53
|
+
统一维护读卡器连接状态,提供 `reader.rf = True / False` 属性控制,
|
|
54
|
+
并将所有属性与方法调用优先路由到当前模块顶层函数,或透传到底层 CardReader 实例。
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._target: Any = None
|
|
59
|
+
self._connected: bool = False
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def rf(self) -> bool:
|
|
63
|
+
"""获取射频场状态。"""
|
|
64
|
+
return getattr(get_reader(), "rf_field", False)
|
|
65
|
+
|
|
66
|
+
@rf.setter
|
|
67
|
+
def rf(self, value: bool) -> None:
|
|
68
|
+
"""设置射频场开关 (True / False)。"""
|
|
69
|
+
get_reader().rf_field = bool(value)
|
|
13
70
|
|
|
14
|
-
|
|
71
|
+
@property
|
|
72
|
+
def rf_field(self) -> bool:
|
|
73
|
+
"""获取射频场状态 (rf 别名)。"""
|
|
74
|
+
return self.rf
|
|
15
75
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
cls._instance._connected = False
|
|
21
|
-
return cls._instance
|
|
76
|
+
@rf_field.setter
|
|
77
|
+
def rf_field(self, value: bool) -> None:
|
|
78
|
+
"""设置射频场开关 (rf 别名)。"""
|
|
79
|
+
self.rf = value
|
|
22
80
|
|
|
23
81
|
@property
|
|
24
|
-
def
|
|
25
|
-
|
|
82
|
+
def mf_crypto(self) -> bool:
|
|
83
|
+
"""获取 Mifare 硬件加密状态。"""
|
|
84
|
+
return getattr(get_reader(), "mf_crypto", False)
|
|
26
85
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
_trace.log(f"Connected to {reader_type.upper()} on port {port}", layer="app")
|
|
86
|
+
@property
|
|
87
|
+
def target(self) -> Any:
|
|
88
|
+
"""获取当前连接的底层 CardReader 实例。"""
|
|
89
|
+
return get_reader()
|
|
32
90
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
def __getattr__(self, name: str) -> Any:
|
|
92
|
+
# 1. 优先路由到当前模块的顶层函数(如 reader.active -> active)
|
|
93
|
+
mod = sys.modules[__name__]
|
|
94
|
+
if hasattr(mod, name) and not name.startswith("_"):
|
|
95
|
+
return getattr(mod, name)
|
|
96
|
+
# 2. 其次透传给底层 CardReader 驱动实例(如 reader.mf_auth -> _target.mf_auth)
|
|
97
|
+
return getattr(self.target, name)
|
|
39
98
|
|
|
40
|
-
def
|
|
41
|
-
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
get_reader()
|
|
101
|
+
return self
|
|
42
102
|
|
|
103
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
104
|
+
pass
|
|
43
105
|
|
|
44
|
-
_state = _NFCState()
|
|
45
106
|
|
|
107
|
+
reader = _ReaderFacade()
|
|
46
108
|
|
|
47
|
-
|
|
109
|
+
|
|
110
|
+
# ─── 模块级顶层函数(单一事实来源) ──────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
def connect(port: str = None, reader_type: str = None) -> None:
|
|
48
113
|
"""
|
|
49
114
|
连接并初始化读卡器。
|
|
50
115
|
|
|
@@ -55,6 +120,7 @@ def connect(port: str = None, reader_type: str = None):
|
|
|
55
120
|
Raises:
|
|
56
121
|
ValueError: 未提供 port 或 reader_type 且环境变量未设置。
|
|
57
122
|
"""
|
|
123
|
+
_load_env_configs()
|
|
58
124
|
if port is None:
|
|
59
125
|
port = os.environ.get("NFC_PORT")
|
|
60
126
|
if reader_type is None:
|
|
@@ -63,18 +129,53 @@ def connect(port: str = None, reader_type: str = None):
|
|
|
63
129
|
raise ValueError("未指定串口,请通过 -p 参数、connect(port=...) 或 NFC_PORT 环境变量配置")
|
|
64
130
|
if reader_type is None:
|
|
65
131
|
raise ValueError("未指定读卡器类型,请通过 -r 参数、connect(reader_type=...) 或 NFC_READER 环境变量配置")
|
|
66
|
-
|
|
132
|
+
|
|
133
|
+
if reader._connected and reader._target is not None:
|
|
134
|
+
disconnect()
|
|
135
|
+
|
|
136
|
+
reader._target = CardReaderRegistry.create(reader_type, transport="serial", port=port)
|
|
137
|
+
reader._target.open()
|
|
138
|
+
reader._connected = True
|
|
139
|
+
_trace.log(f"Connected to {reader_type.upper()} on port {port}", layer="app")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def disconnect() -> None:
|
|
143
|
+
"""断开与读卡器的连接并释放资源。"""
|
|
144
|
+
if reader._connected and reader._target is not None:
|
|
145
|
+
try:
|
|
146
|
+
reader._target.rf_field = False
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
try:
|
|
150
|
+
reader._target.close()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
reader._connected = False
|
|
154
|
+
reader._target = None
|
|
155
|
+
_trace.log("Disconnected", layer="app")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def close() -> None:
|
|
159
|
+
"""断开与读卡器的连接 (disconnect 别名)。"""
|
|
160
|
+
disconnect()
|
|
67
161
|
|
|
68
162
|
|
|
69
|
-
def get_reader():
|
|
163
|
+
def get_reader() -> Any:
|
|
70
164
|
"""
|
|
71
|
-
|
|
165
|
+
获取当前连接的读卡器实例,若未连接则尝试从环境变量自动连接。
|
|
72
166
|
|
|
73
167
|
Returns:
|
|
74
168
|
reader: 当前连接的读卡器实例。
|
|
75
169
|
"""
|
|
76
|
-
|
|
77
|
-
|
|
170
|
+
if not reader._connected or reader._target is None:
|
|
171
|
+
_load_env_configs()
|
|
172
|
+
port = os.environ.get("NFC_PORT")
|
|
173
|
+
reader_type = os.environ.get("NFC_READER")
|
|
174
|
+
if port and reader_type:
|
|
175
|
+
connect(port, reader_type)
|
|
176
|
+
else:
|
|
177
|
+
ASSERT_IS_NOT_NONE(reader._target, msg="NFC driver not connected. Call connect() first.")
|
|
178
|
+
return reader._target
|
|
78
179
|
|
|
79
180
|
|
|
80
181
|
def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -> CardInfo | None:
|
|
@@ -94,11 +195,10 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
|
|
|
94
195
|
- 'atq' (list[int]): 卡片的 ATQ (Answer To Request)。
|
|
95
196
|
- 'sak' (int): 卡片的 SAK (Select Acknowledge)。
|
|
96
197
|
"""
|
|
97
|
-
|
|
98
|
-
reader = _state.reader
|
|
198
|
+
rdr = get_reader()
|
|
99
199
|
|
|
100
200
|
if not low_layer:
|
|
101
|
-
return
|
|
201
|
+
return rdr.active()
|
|
102
202
|
|
|
103
203
|
# 手动底层寻卡流程
|
|
104
204
|
# 1. REQA
|
|
@@ -109,6 +209,7 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
|
|
|
109
209
|
|
|
110
210
|
# 2. 抗冲突与选择
|
|
111
211
|
full_uid = []
|
|
212
|
+
sak = 0
|
|
112
213
|
for cl in [1, 2, 3]:
|
|
113
214
|
res = anticoll(cl_level=cl, nvb=0x20)
|
|
114
215
|
if not res.data:
|
|
@@ -153,30 +254,35 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
|
|
|
153
254
|
return info
|
|
154
255
|
|
|
155
256
|
|
|
156
|
-
def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
|
|
257
|
+
def transceive(data: list[int] | bytes, tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
|
|
157
258
|
"""
|
|
158
259
|
使用底层帧交互方式发送数据。
|
|
159
260
|
|
|
160
261
|
Args:
|
|
161
|
-
data:
|
|
262
|
+
data: 要发送的字节列表或 bytes。
|
|
162
263
|
tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
|
|
163
264
|
rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
|
|
164
265
|
|
|
165
266
|
Returns:
|
|
166
267
|
list[int]: 接收到的数据字节列表。
|
|
167
268
|
"""
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
res =
|
|
269
|
+
rdr = get_reader()
|
|
270
|
+
raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
|
|
271
|
+
res = rdr.transceive(raw_list, tx_crc=tx_crc, rx_crc=rx_crc)
|
|
171
272
|
return res.data if res and res.data else []
|
|
172
273
|
|
|
173
274
|
|
|
174
|
-
def transceive_bits(
|
|
275
|
+
def transceive_bits(
|
|
276
|
+
data: list[int] | bytes,
|
|
277
|
+
last_tx_bits: int = 0,
|
|
278
|
+
tx_crc: bool = True,
|
|
279
|
+
rx_crc: bool = True,
|
|
280
|
+
) -> TransceiveBits:
|
|
175
281
|
"""
|
|
176
282
|
使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
|
|
177
283
|
|
|
178
284
|
Args:
|
|
179
|
-
data:
|
|
285
|
+
data: 要发送的字节列表或 bytes。
|
|
180
286
|
last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),0 表示发送完整字节。
|
|
181
287
|
tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
|
|
182
288
|
rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
|
|
@@ -184,9 +290,9 @@ def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True,
|
|
|
184
290
|
Returns:
|
|
185
291
|
TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
|
|
186
292
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
res =
|
|
293
|
+
rdr = get_reader()
|
|
294
|
+
raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
|
|
295
|
+
res = rdr.transceive(raw_list, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
|
|
190
296
|
return res if res else TransceiveBits(data=[], bits=0)
|
|
191
297
|
|
|
192
298
|
|
|
@@ -273,33 +379,24 @@ def select(cl_level: int, uid: list[int]) -> list[int]:
|
|
|
273
379
|
return transceive(cmd, tx_crc=True, rx_crc=True)
|
|
274
380
|
|
|
275
381
|
|
|
276
|
-
def field_on():
|
|
382
|
+
def field_on() -> None:
|
|
277
383
|
"""
|
|
278
|
-
|
|
384
|
+
开启读卡器的 RF 场。
|
|
279
385
|
|
|
280
386
|
Raises:
|
|
281
387
|
AssertionError: 未连接读卡器时抛出。
|
|
282
388
|
"""
|
|
283
|
-
|
|
284
|
-
_state.reader.rf_field = True
|
|
389
|
+
get_reader().rf_field = True
|
|
285
390
|
|
|
286
391
|
|
|
287
|
-
def field_off():
|
|
392
|
+
def field_off() -> None:
|
|
288
393
|
"""
|
|
289
|
-
|
|
394
|
+
关闭读卡器的 RF 场。
|
|
290
395
|
|
|
291
396
|
Raises:
|
|
292
397
|
AssertionError: 未连接读卡器时抛出。
|
|
293
398
|
"""
|
|
294
|
-
|
|
295
|
-
_state.reader.rf_field = False
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def close():
|
|
299
|
-
"""
|
|
300
|
-
断开与 PN532 读卡器的连接。
|
|
301
|
-
"""
|
|
302
|
-
_state.disconnect()
|
|
399
|
+
get_reader().rf_field = False
|
|
303
400
|
|
|
304
401
|
|
|
305
402
|
@contextmanager
|
|
@@ -308,8 +405,8 @@ def session(port: str = None, reader_type: str = None):
|
|
|
308
405
|
上下文管理器,自动管理连接生命周期。
|
|
309
406
|
|
|
310
407
|
用法:
|
|
311
|
-
with session("COM20") as
|
|
312
|
-
card_info =
|
|
408
|
+
with session("COM20") as r:
|
|
409
|
+
card_info = r.active()
|
|
313
410
|
"""
|
|
314
411
|
if port is None:
|
|
315
412
|
port = os.environ.get("NFC_PORT")
|
|
@@ -319,8 +416,44 @@ def session(port: str = None, reader_type: str = None):
|
|
|
319
416
|
raise ValueError("未指定串口,请通过 session(port=...) 或 NFC_PORT 环境变量配置")
|
|
320
417
|
if reader_type is None:
|
|
321
418
|
raise ValueError("未指定读卡器类型,请通过 session(reader_type=...) 或 NFC_READER 环境变量配置")
|
|
322
|
-
|
|
419
|
+
connect(port, reader_type)
|
|
323
420
|
try:
|
|
324
|
-
yield
|
|
421
|
+
yield reader
|
|
325
422
|
finally:
|
|
326
|
-
|
|
423
|
+
disconnect()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _atexit_cleanup() -> None:
|
|
427
|
+
"""进程退出时的 atexit 安全清理钩子:先关闭物理 RF 场,再关闭串口。"""
|
|
428
|
+
if reader._connected and reader._target is not None:
|
|
429
|
+
try:
|
|
430
|
+
reader._target.rf_field = False
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
try:
|
|
434
|
+
reader._target.close()
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
atexit.register(_atexit_cleanup)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
__all__ = [
|
|
443
|
+
"reader",
|
|
444
|
+
"connect",
|
|
445
|
+
"disconnect",
|
|
446
|
+
"get_reader",
|
|
447
|
+
"active",
|
|
448
|
+
"transceive",
|
|
449
|
+
"transceive_bits",
|
|
450
|
+
"reqa",
|
|
451
|
+
"wupa",
|
|
452
|
+
"halt",
|
|
453
|
+
"anticoll",
|
|
454
|
+
"select",
|
|
455
|
+
"field_on",
|
|
456
|
+
"field_off",
|
|
457
|
+
"close",
|
|
458
|
+
"session",
|
|
459
|
+
]
|
|
@@ -1,60 +1,70 @@
|
|
|
1
1
|
"""reader 模块测试 (connect / get_reader / active / transceive / session 等)"""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
2
4
|
import pytest
|
|
3
|
-
from unittest.mock import MagicMock, patch
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
import nfc
|
|
4
7
|
from nfc import reader
|
|
5
8
|
from nfctester.drivers.card_reader import CardInfo, TransceiveBits
|
|
9
|
+
from nfctester.registry import CardReaderRegistry
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
@pytest.fixture(autouse=True)
|
|
9
13
|
def reset_state():
|
|
10
14
|
"""每个测试前重置全局状态"""
|
|
11
|
-
reader.
|
|
12
|
-
reader.
|
|
15
|
+
reader._target = None
|
|
16
|
+
reader._connected = False
|
|
13
17
|
yield
|
|
14
|
-
reader.
|
|
15
|
-
reader.
|
|
18
|
+
reader._target = None
|
|
19
|
+
reader._connected = False
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
def _mock_reader():
|
|
19
23
|
mock = MagicMock()
|
|
20
24
|
mock.transceive.return_value = TransceiveBits(data=[0xAA, 0xBB], bits=8)
|
|
21
25
|
mock.active.return_value = CardInfo(uid=[0x01, 0x02, 0x03, 0x04], atq=[0x44, 0x00], sak=0x00)
|
|
26
|
+
mock.rf_field = False
|
|
27
|
+
mock.mf_crypto = False
|
|
22
28
|
return mock
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
class TestConnect:
|
|
26
32
|
|
|
27
|
-
@patch.object(
|
|
33
|
+
@patch.object(CardReaderRegistry, "create")
|
|
28
34
|
def test_connect_with_params(self, mock_create):
|
|
29
|
-
|
|
30
|
-
mock_create.return_value =
|
|
31
|
-
|
|
35
|
+
mock_rdr = _mock_reader()
|
|
36
|
+
mock_create.return_value = mock_rdr
|
|
37
|
+
nfc.connect(port="COM99", reader_type="pn532")
|
|
32
38
|
mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
|
|
33
|
-
|
|
34
|
-
assert reader.
|
|
39
|
+
mock_rdr.open.assert_called_once()
|
|
40
|
+
assert reader._connected is True
|
|
35
41
|
|
|
36
|
-
@patch.object(
|
|
42
|
+
@patch.object(CardReaderRegistry, "create")
|
|
37
43
|
def test_connect_from_env(self, mock_create, monkeypatch):
|
|
38
44
|
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
39
45
|
monkeypatch.setenv("NFC_READER", "pn532")
|
|
40
46
|
mock_create.return_value = _mock_reader()
|
|
41
|
-
|
|
47
|
+
nfc.connect()
|
|
42
48
|
mock_create.assert_called_once_with("pn532", transport="serial", port="COM10")
|
|
43
49
|
|
|
44
|
-
def test_connect_no_port_raises(self):
|
|
50
|
+
def test_connect_no_port_raises(self, monkeypatch):
|
|
51
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
52
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
45
53
|
with pytest.raises(ValueError, match="未指定串口"):
|
|
46
|
-
|
|
54
|
+
nfc.connect(reader_type="pn532")
|
|
47
55
|
|
|
48
|
-
def test_connect_no_reader_type_raises(self):
|
|
56
|
+
def test_connect_no_reader_type_raises(self, monkeypatch):
|
|
57
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
58
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
49
59
|
with pytest.raises(ValueError, match="未指定读卡器类型"):
|
|
50
|
-
|
|
60
|
+
nfc.connect(port="COM99")
|
|
51
61
|
|
|
52
|
-
@patch.object(
|
|
62
|
+
@patch.object(CardReaderRegistry, "create")
|
|
53
63
|
def test_connect_overrides_env(self, mock_create, monkeypatch):
|
|
54
64
|
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
55
65
|
monkeypatch.setenv("NFC_READER", "clrc663")
|
|
56
66
|
mock_create.return_value = _mock_reader()
|
|
57
|
-
|
|
67
|
+
nfc.connect(port="COM99", reader_type="pn532")
|
|
58
68
|
mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
|
|
59
69
|
|
|
60
70
|
|
|
@@ -62,123 +72,128 @@ class TestGetReader:
|
|
|
62
72
|
|
|
63
73
|
def test_get_reader_when_connected(self):
|
|
64
74
|
mock = _mock_reader()
|
|
65
|
-
reader.
|
|
66
|
-
reader.
|
|
67
|
-
assert
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
reader._target = mock
|
|
76
|
+
reader._connected = True
|
|
77
|
+
assert nfc.get_reader() is mock
|
|
78
|
+
assert reader.target is mock
|
|
79
|
+
|
|
80
|
+
def test_get_reader_when_not_connected(self, monkeypatch):
|
|
81
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
82
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
70
83
|
with pytest.raises(AssertionError):
|
|
71
|
-
|
|
84
|
+
nfc.get_reader()
|
|
72
85
|
|
|
73
86
|
|
|
74
87
|
class TestTransceive:
|
|
75
88
|
|
|
76
89
|
def test_transceive_returns_data(self):
|
|
77
90
|
mock = _mock_reader()
|
|
78
|
-
reader.
|
|
79
|
-
reader.
|
|
80
|
-
result =
|
|
91
|
+
reader._target = mock
|
|
92
|
+
reader._connected = True
|
|
93
|
+
result = nfc.transceive([0x30, 0x00])
|
|
81
94
|
assert result == [0xAA, 0xBB]
|
|
82
95
|
mock.transceive.assert_called_once_with([0x30, 0x00], tx_crc=True, rx_crc=True)
|
|
83
96
|
|
|
84
97
|
def test_transceive_no_crc(self):
|
|
85
98
|
mock = _mock_reader()
|
|
86
|
-
reader.
|
|
87
|
-
reader.
|
|
88
|
-
|
|
99
|
+
reader._target = mock
|
|
100
|
+
reader._connected = True
|
|
101
|
+
nfc.transceive([0x30], tx_crc=False, rx_crc=False)
|
|
89
102
|
mock.transceive.assert_called_once_with([0x30], tx_crc=False, rx_crc=False)
|
|
90
103
|
|
|
91
104
|
def test_transceive_empty_result(self):
|
|
92
105
|
mock = _mock_reader()
|
|
93
106
|
mock.transceive.return_value = None
|
|
94
|
-
reader.
|
|
95
|
-
reader.
|
|
96
|
-
result =
|
|
107
|
+
reader._target = mock
|
|
108
|
+
reader._connected = True
|
|
109
|
+
result = nfc.transceive([0x30])
|
|
97
110
|
assert result == []
|
|
98
111
|
|
|
99
|
-
def test_transceive_not_connected(self):
|
|
112
|
+
def test_transceive_not_connected(self, monkeypatch):
|
|
113
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
114
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
100
115
|
with pytest.raises(AssertionError):
|
|
101
|
-
|
|
116
|
+
nfc.transceive([0x30])
|
|
102
117
|
|
|
103
118
|
|
|
104
119
|
class TestTransceiveBits:
|
|
105
120
|
|
|
106
121
|
def test_transceive_bits_returns_result(self):
|
|
107
122
|
mock = _mock_reader()
|
|
108
|
-
reader.
|
|
109
|
-
reader.
|
|
110
|
-
result =
|
|
123
|
+
reader._target = mock
|
|
124
|
+
reader._connected = True
|
|
125
|
+
result = nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
111
126
|
assert result is not None
|
|
112
127
|
assert result.data == [0xAA, 0xBB]
|
|
113
128
|
|
|
114
129
|
def test_transceive_bits_passes_params(self):
|
|
115
130
|
mock = _mock_reader()
|
|
116
|
-
reader.
|
|
117
|
-
reader.
|
|
118
|
-
|
|
131
|
+
reader._target = mock
|
|
132
|
+
reader._connected = True
|
|
133
|
+
nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
119
134
|
mock.transceive.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
120
135
|
|
|
121
136
|
|
|
122
137
|
class TestReqa:
|
|
123
138
|
|
|
124
|
-
@patch.
|
|
139
|
+
@patch("nfc.reader.transceive_bits")
|
|
125
140
|
def test_reqa_default(self, mock_tb):
|
|
126
141
|
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
127
|
-
result =
|
|
142
|
+
result = nfc.reqa()
|
|
128
143
|
mock_tb.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
129
144
|
assert result is not None
|
|
130
145
|
|
|
131
|
-
@patch.
|
|
146
|
+
@patch("nfc.reader.transceive_bits")
|
|
132
147
|
def test_reqa_custom_cmd(self, mock_tb):
|
|
133
148
|
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
134
|
-
|
|
149
|
+
nfc.reqa(cmd=0x20)
|
|
135
150
|
mock_tb.assert_called_once_with([0x20], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
136
151
|
|
|
137
152
|
|
|
138
153
|
class TestWupa:
|
|
139
154
|
|
|
140
|
-
@patch.
|
|
155
|
+
@patch("nfc.reader.transceive_bits")
|
|
141
156
|
def test_wupa(self, mock_tb):
|
|
142
157
|
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
143
|
-
result =
|
|
158
|
+
result = nfc.wupa()
|
|
144
159
|
mock_tb.assert_called_once_with([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
145
160
|
assert result is not None
|
|
146
161
|
|
|
147
162
|
|
|
148
163
|
class TestHalt:
|
|
149
164
|
|
|
150
|
-
@patch.
|
|
165
|
+
@patch("nfc.reader.transceive")
|
|
151
166
|
def test_halt(self, mock_ts):
|
|
152
167
|
mock_ts.return_value = []
|
|
153
|
-
result =
|
|
168
|
+
result = nfc.halt()
|
|
154
169
|
mock_ts.assert_called_once_with([0x50, 0x00], tx_crc=True, rx_crc=True)
|
|
155
170
|
|
|
156
171
|
|
|
157
172
|
class TestSelect:
|
|
158
173
|
|
|
159
|
-
@patch.
|
|
174
|
+
@patch("nfc.reader.transceive")
|
|
160
175
|
def test_select_cl1(self, mock_ts):
|
|
161
176
|
mock_ts.return_value = [0x08]
|
|
162
|
-
result =
|
|
177
|
+
result = nfc.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
163
178
|
mock_ts.assert_called_once_with(
|
|
164
179
|
[0x93, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
165
180
|
tx_crc=True, rx_crc=True
|
|
166
181
|
)
|
|
167
182
|
assert result == [0x08]
|
|
168
183
|
|
|
169
|
-
@patch.
|
|
184
|
+
@patch("nfc.reader.transceive")
|
|
170
185
|
def test_select_cl2(self, mock_ts):
|
|
171
186
|
mock_ts.return_value = [0x08]
|
|
172
|
-
|
|
187
|
+
nfc.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
173
188
|
mock_ts.assert_called_once_with(
|
|
174
189
|
[0x95, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
175
190
|
tx_crc=True, rx_crc=True
|
|
176
191
|
)
|
|
177
192
|
|
|
178
|
-
@patch.
|
|
193
|
+
@patch("nfc.reader.transceive")
|
|
179
194
|
def test_select_cl3(self, mock_ts):
|
|
180
195
|
mock_ts.return_value = [0x08]
|
|
181
|
-
|
|
196
|
+
nfc.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
182
197
|
mock_ts.assert_called_once_with(
|
|
183
198
|
[0x97, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
184
199
|
tx_crc=True, rx_crc=True
|
|
@@ -186,28 +201,28 @@ class TestSelect:
|
|
|
186
201
|
|
|
187
202
|
def test_select_invalid_cl(self):
|
|
188
203
|
with pytest.raises(ValueError, match="Invalid CL level"):
|
|
189
|
-
|
|
204
|
+
nfc.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
190
205
|
|
|
191
206
|
|
|
192
207
|
class TestAnticoll:
|
|
193
208
|
|
|
194
|
-
@patch.
|
|
209
|
+
@patch("nfc.reader.transceive_bits")
|
|
195
210
|
def test_anticoll_cl1(self, mock_tb):
|
|
196
211
|
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
197
|
-
result =
|
|
212
|
+
result = nfc.anticoll(cl_level=1)
|
|
198
213
|
mock_tb.assert_called_once_with([0x93, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
199
214
|
assert result is not None
|
|
200
215
|
|
|
201
|
-
@patch.
|
|
216
|
+
@patch("nfc.reader.transceive_bits")
|
|
202
217
|
def test_anticoll_cl2(self, mock_tb):
|
|
203
218
|
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
204
|
-
|
|
219
|
+
nfc.anticoll(cl_level=2)
|
|
205
220
|
mock_tb.assert_called_once_with([0x95, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
206
221
|
|
|
207
|
-
@patch.
|
|
222
|
+
@patch("nfc.reader.transceive_bits")
|
|
208
223
|
def test_anticoll_with_uid_prefix(self, mock_tb):
|
|
209
224
|
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
210
|
-
|
|
225
|
+
nfc.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
|
|
211
226
|
mock_tb.assert_called_once_with(
|
|
212
227
|
[0x93, 0x20, 0x04, 0x12, 0x34, 0x56],
|
|
213
228
|
last_tx_bits=0, tx_crc=False, rx_crc=False
|
|
@@ -218,58 +233,99 @@ class TestFieldControl:
|
|
|
218
233
|
|
|
219
234
|
def test_field_on(self):
|
|
220
235
|
mock = _mock_reader()
|
|
221
|
-
reader.
|
|
222
|
-
reader.
|
|
223
|
-
|
|
236
|
+
reader._target = mock
|
|
237
|
+
reader._connected = True
|
|
238
|
+
nfc.field_on()
|
|
224
239
|
assert mock.rf_field is True
|
|
225
240
|
|
|
226
241
|
def test_field_off(self):
|
|
227
242
|
mock = _mock_reader()
|
|
228
|
-
reader.
|
|
229
|
-
reader.
|
|
230
|
-
|
|
243
|
+
reader._target = mock
|
|
244
|
+
reader._connected = True
|
|
245
|
+
nfc.field_off()
|
|
231
246
|
assert mock.rf_field is False
|
|
232
247
|
|
|
233
248
|
|
|
234
249
|
class TestSession:
|
|
235
250
|
|
|
236
|
-
@patch.
|
|
237
|
-
@patch.
|
|
251
|
+
@patch("nfc.reader.disconnect")
|
|
252
|
+
@patch("nfc.reader.connect")
|
|
238
253
|
def test_session_yields_reader(self, mock_connect, mock_disconnect):
|
|
239
254
|
mock_rdr = _mock_reader()
|
|
240
255
|
|
|
241
256
|
def fake_connect(port="COM20", reader_type="pn532"):
|
|
242
|
-
reader.
|
|
243
|
-
reader.
|
|
257
|
+
reader._target = mock_rdr
|
|
258
|
+
reader._connected = True
|
|
244
259
|
|
|
245
260
|
mock_connect.side_effect = fake_connect
|
|
246
261
|
|
|
247
|
-
with
|
|
248
|
-
assert r is
|
|
262
|
+
with nfc.session(port="COM99", reader_type="pn532") as r:
|
|
263
|
+
assert r is reader
|
|
249
264
|
mock_disconnect.assert_called_once()
|
|
250
265
|
|
|
251
|
-
def test_session_no_port_raises(self):
|
|
266
|
+
def test_session_no_port_raises(self, monkeypatch):
|
|
267
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
268
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
252
269
|
with pytest.raises(ValueError, match="未指定串口"):
|
|
253
|
-
with
|
|
270
|
+
with nfc.session(reader_type="pn532"):
|
|
254
271
|
pass
|
|
255
272
|
|
|
256
|
-
def test_session_no_reader_type_raises(self):
|
|
273
|
+
def test_session_no_reader_type_raises(self, monkeypatch):
|
|
274
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
275
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
257
276
|
with pytest.raises(ValueError, match="未指定读卡器类型"):
|
|
258
|
-
with
|
|
277
|
+
with nfc.session(port="COM99"):
|
|
259
278
|
pass
|
|
260
279
|
|
|
261
280
|
|
|
262
281
|
class TestGetSelCmd:
|
|
263
282
|
|
|
264
283
|
def test_cl1(self):
|
|
265
|
-
|
|
284
|
+
from nfc.reader import _get_sel_cmd
|
|
285
|
+
assert _get_sel_cmd(1) == 0x93
|
|
266
286
|
|
|
267
287
|
def test_cl2(self):
|
|
268
|
-
|
|
288
|
+
from nfc.reader import _get_sel_cmd
|
|
289
|
+
assert _get_sel_cmd(2) == 0x95
|
|
269
290
|
|
|
270
291
|
def test_cl3(self):
|
|
271
|
-
|
|
292
|
+
from nfc.reader import _get_sel_cmd
|
|
293
|
+
assert _get_sel_cmd(3) == 0x97
|
|
272
294
|
|
|
273
295
|
def test_invalid(self):
|
|
296
|
+
from nfc.reader import _get_sel_cmd
|
|
274
297
|
with pytest.raises(ValueError):
|
|
275
|
-
|
|
298
|
+
_get_sel_cmd(4)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class TestReaderSingletonProxy:
|
|
302
|
+
|
|
303
|
+
@patch.object(CardReaderRegistry, "create")
|
|
304
|
+
def test_reader_rf_property_and_lazy_connect(self, mock_create, monkeypatch):
|
|
305
|
+
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
306
|
+
monkeypatch.setenv("NFC_READER", "pn532")
|
|
307
|
+
mock_rdr = _mock_reader()
|
|
308
|
+
mock_create.return_value = mock_rdr
|
|
309
|
+
|
|
310
|
+
# reader.rf triggers auto-connect
|
|
311
|
+
reader.rf = True
|
|
312
|
+
assert mock_rdr.rf_field is True
|
|
313
|
+
assert reader.rf is True
|
|
314
|
+
assert reader.rf_field is True
|
|
315
|
+
assert reader.mf_crypto is False
|
|
316
|
+
|
|
317
|
+
@patch.object(CardReaderRegistry, "create")
|
|
318
|
+
def test_reader_delegates_to_functions_and_target(self, mock_create, monkeypatch):
|
|
319
|
+
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
320
|
+
monkeypatch.setenv("NFC_READER", "pn532")
|
|
321
|
+
mock_rdr = _mock_reader()
|
|
322
|
+
mock_create.return_value = mock_rdr
|
|
323
|
+
|
|
324
|
+
# reader.active calls active()
|
|
325
|
+
card = reader.active()
|
|
326
|
+
assert card is not None
|
|
327
|
+
assert card.uid == [0x01, 0x02, 0x03, 0x04]
|
|
328
|
+
|
|
329
|
+
# reader.custom_method delegates to mock_rdr
|
|
330
|
+
mock_rdr.custom_method = MagicMock(return_value="OK")
|
|
331
|
+
assert reader.custom_method() == "OK"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|