nfcscript 0.0.16__tar.gz → 0.0.18__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.18}/PKG-INFO +1 -1
- {nfcscript-0.0.16 → nfcscript-0.0.18}/pyproject.toml +2 -5
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/nfc_cli.py +3 -3
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/reader.py +205 -64
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/PKG-INFO +1 -1
- nfcscript-0.0.18/tests/test_reader.py +338 -0
- nfcscript-0.0.16/tests/test_reader.py +0 -275
- {nfcscript-0.0.16 → nfcscript-0.0.18}/README.md +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/setup.cfg +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/__init__.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/assertions.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/bits.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/checksum.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/cli.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/delay.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/hex_util.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfc/trace.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/SOURCES.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/dependency_links.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/entry_points.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/requires.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/src/nfcscript.egg-info/top_level.txt +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_assertions.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_bits.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_checksum.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_cli.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_cli_collect.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_delay.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/tests/test_hex_util.py +0 -0
- {nfcscript-0.0.16 → nfcscript-0.0.18}/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.18"
|
|
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.18"
|
|
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,40 +254,53 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
|
|
|
153
254
|
return info
|
|
154
255
|
|
|
155
256
|
|
|
156
|
-
def transceive(
|
|
257
|
+
def transceive(
|
|
258
|
+
data: list[int] | bytes,
|
|
259
|
+
tx_crc: bool = True,
|
|
260
|
+
rx_crc: bool = True,
|
|
261
|
+
timeout: float = 0.5,
|
|
262
|
+
) -> list[int]:
|
|
157
263
|
"""
|
|
158
264
|
使用底层帧交互方式发送数据。
|
|
159
265
|
|
|
160
266
|
Args:
|
|
161
|
-
data:
|
|
267
|
+
data: 要发送的字节列表或 bytes。
|
|
162
268
|
tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
|
|
163
269
|
rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
|
|
270
|
+
timeout: 接收超时时间 (秒),默认 0.5s。
|
|
164
271
|
|
|
165
272
|
Returns:
|
|
166
273
|
list[int]: 接收到的数据字节列表。
|
|
167
274
|
"""
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
res =
|
|
275
|
+
rdr = get_reader()
|
|
276
|
+
raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
|
|
277
|
+
res = rdr.transceive(raw_list, tx_crc=tx_crc, rx_crc=rx_crc, timeout=timeout)
|
|
171
278
|
return res.data if res and res.data else []
|
|
172
279
|
|
|
173
280
|
|
|
174
|
-
def transceive_bits(
|
|
281
|
+
def transceive_bits(
|
|
282
|
+
data: list[int] | bytes,
|
|
283
|
+
last_tx_bits: int = 0,
|
|
284
|
+
tx_crc: bool = True,
|
|
285
|
+
rx_crc: bool = True,
|
|
286
|
+
timeout: float = 0.5,
|
|
287
|
+
) -> TransceiveBits:
|
|
175
288
|
"""
|
|
176
289
|
使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
|
|
177
290
|
|
|
178
291
|
Args:
|
|
179
|
-
data:
|
|
292
|
+
data: 要发送的字节列表或 bytes。
|
|
180
293
|
last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),0 表示发送完整字节。
|
|
181
294
|
tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
|
|
182
295
|
rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
|
|
296
|
+
timeout: 接收超时时间 (秒),默认 0.5s。
|
|
183
297
|
|
|
184
298
|
Returns:
|
|
185
299
|
TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
|
|
186
300
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
res =
|
|
301
|
+
rdr = get_reader()
|
|
302
|
+
raw_list = list(data) if isinstance(data, (bytes, bytearray)) else data
|
|
303
|
+
res = rdr.transceive(raw_list, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc, timeout=timeout)
|
|
190
304
|
return res if res else TransceiveBits(data=[], bits=0)
|
|
191
305
|
|
|
192
306
|
|
|
@@ -273,33 +387,24 @@ def select(cl_level: int, uid: list[int]) -> list[int]:
|
|
|
273
387
|
return transceive(cmd, tx_crc=True, rx_crc=True)
|
|
274
388
|
|
|
275
389
|
|
|
276
|
-
def field_on():
|
|
390
|
+
def field_on() -> None:
|
|
277
391
|
"""
|
|
278
|
-
|
|
392
|
+
开启读卡器的 RF 场。
|
|
279
393
|
|
|
280
394
|
Raises:
|
|
281
395
|
AssertionError: 未连接读卡器时抛出。
|
|
282
396
|
"""
|
|
283
|
-
|
|
284
|
-
_state.reader.rf_field = True
|
|
397
|
+
get_reader().rf_field = True
|
|
285
398
|
|
|
286
399
|
|
|
287
|
-
def field_off():
|
|
400
|
+
def field_off() -> None:
|
|
288
401
|
"""
|
|
289
|
-
|
|
402
|
+
关闭读卡器的 RF 场。
|
|
290
403
|
|
|
291
404
|
Raises:
|
|
292
405
|
AssertionError: 未连接读卡器时抛出。
|
|
293
406
|
"""
|
|
294
|
-
|
|
295
|
-
_state.reader.rf_field = False
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def close():
|
|
299
|
-
"""
|
|
300
|
-
断开与 PN532 读卡器的连接。
|
|
301
|
-
"""
|
|
302
|
-
_state.disconnect()
|
|
407
|
+
get_reader().rf_field = False
|
|
303
408
|
|
|
304
409
|
|
|
305
410
|
@contextmanager
|
|
@@ -308,8 +413,8 @@ def session(port: str = None, reader_type: str = None):
|
|
|
308
413
|
上下文管理器,自动管理连接生命周期。
|
|
309
414
|
|
|
310
415
|
用法:
|
|
311
|
-
with session("COM20") as
|
|
312
|
-
card_info =
|
|
416
|
+
with session("COM20") as r:
|
|
417
|
+
card_info = r.active()
|
|
313
418
|
"""
|
|
314
419
|
if port is None:
|
|
315
420
|
port = os.environ.get("NFC_PORT")
|
|
@@ -319,8 +424,44 @@ def session(port: str = None, reader_type: str = None):
|
|
|
319
424
|
raise ValueError("未指定串口,请通过 session(port=...) 或 NFC_PORT 环境变量配置")
|
|
320
425
|
if reader_type is None:
|
|
321
426
|
raise ValueError("未指定读卡器类型,请通过 session(reader_type=...) 或 NFC_READER 环境变量配置")
|
|
322
|
-
|
|
427
|
+
connect(port, reader_type)
|
|
323
428
|
try:
|
|
324
|
-
yield
|
|
429
|
+
yield reader
|
|
325
430
|
finally:
|
|
326
|
-
|
|
431
|
+
disconnect()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _atexit_cleanup() -> None:
|
|
435
|
+
"""进程退出时的 atexit 安全清理钩子:先关闭物理 RF 场,再关闭串口。"""
|
|
436
|
+
if reader._connected and reader._target is not None:
|
|
437
|
+
try:
|
|
438
|
+
reader._target.rf_field = False
|
|
439
|
+
except Exception:
|
|
440
|
+
pass
|
|
441
|
+
try:
|
|
442
|
+
reader._target.close()
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
atexit.register(_atexit_cleanup)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
__all__ = [
|
|
451
|
+
"reader",
|
|
452
|
+
"connect",
|
|
453
|
+
"disconnect",
|
|
454
|
+
"get_reader",
|
|
455
|
+
"active",
|
|
456
|
+
"transceive",
|
|
457
|
+
"transceive_bits",
|
|
458
|
+
"reqa",
|
|
459
|
+
"wupa",
|
|
460
|
+
"halt",
|
|
461
|
+
"anticoll",
|
|
462
|
+
"select",
|
|
463
|
+
"field_on",
|
|
464
|
+
"field_off",
|
|
465
|
+
"close",
|
|
466
|
+
"session",
|
|
467
|
+
]
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""reader 模块测试 (connect / get_reader / active / transceive / session 等)"""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
import nfc
|
|
7
|
+
from nfc import reader
|
|
8
|
+
from nfctester.drivers.card_reader import CardInfo, TransceiveBits
|
|
9
|
+
from nfctester.registry import CardReaderRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def reset_state():
|
|
14
|
+
"""每个测试前重置全局状态"""
|
|
15
|
+
reader._target = None
|
|
16
|
+
reader._connected = False
|
|
17
|
+
yield
|
|
18
|
+
reader._target = None
|
|
19
|
+
reader._connected = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _mock_reader():
|
|
23
|
+
mock = MagicMock()
|
|
24
|
+
mock.transceive.return_value = TransceiveBits(data=[0xAA, 0xBB], bits=8)
|
|
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
|
|
28
|
+
return mock
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestConnect:
|
|
32
|
+
|
|
33
|
+
@patch.object(CardReaderRegistry, "create")
|
|
34
|
+
def test_connect_with_params(self, mock_create):
|
|
35
|
+
mock_rdr = _mock_reader()
|
|
36
|
+
mock_create.return_value = mock_rdr
|
|
37
|
+
nfc.connect(port="COM99", reader_type="pn532")
|
|
38
|
+
mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
|
|
39
|
+
mock_rdr.open.assert_called_once()
|
|
40
|
+
assert reader._connected is True
|
|
41
|
+
|
|
42
|
+
@patch.object(CardReaderRegistry, "create")
|
|
43
|
+
def test_connect_from_env(self, mock_create, monkeypatch):
|
|
44
|
+
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
45
|
+
monkeypatch.setenv("NFC_READER", "pn532")
|
|
46
|
+
mock_create.return_value = _mock_reader()
|
|
47
|
+
nfc.connect()
|
|
48
|
+
mock_create.assert_called_once_with("pn532", transport="serial", port="COM10")
|
|
49
|
+
|
|
50
|
+
def test_connect_no_port_raises(self, monkeypatch):
|
|
51
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
52
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
53
|
+
with pytest.raises(ValueError, match="未指定串口"):
|
|
54
|
+
nfc.connect(reader_type="pn532")
|
|
55
|
+
|
|
56
|
+
def test_connect_no_reader_type_raises(self, monkeypatch):
|
|
57
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
58
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
59
|
+
with pytest.raises(ValueError, match="未指定读卡器类型"):
|
|
60
|
+
nfc.connect(port="COM99")
|
|
61
|
+
|
|
62
|
+
@patch.object(CardReaderRegistry, "create")
|
|
63
|
+
def test_connect_overrides_env(self, mock_create, monkeypatch):
|
|
64
|
+
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
65
|
+
monkeypatch.setenv("NFC_READER", "clrc663")
|
|
66
|
+
mock_create.return_value = _mock_reader()
|
|
67
|
+
nfc.connect(port="COM99", reader_type="pn532")
|
|
68
|
+
mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestGetReader:
|
|
72
|
+
|
|
73
|
+
def test_get_reader_when_connected(self):
|
|
74
|
+
mock = _mock_reader()
|
|
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)
|
|
83
|
+
with pytest.raises(AssertionError):
|
|
84
|
+
nfc.get_reader()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestTransceive:
|
|
88
|
+
|
|
89
|
+
def test_transceive_returns_data(self):
|
|
90
|
+
mock = _mock_reader()
|
|
91
|
+
reader._target = mock
|
|
92
|
+
reader._connected = True
|
|
93
|
+
result = nfc.transceive([0x30, 0x00])
|
|
94
|
+
assert result == [0xAA, 0xBB]
|
|
95
|
+
mock.transceive.assert_called_once_with([0x30, 0x00], tx_crc=True, rx_crc=True, timeout=0.5)
|
|
96
|
+
|
|
97
|
+
def test_transceive_no_crc(self):
|
|
98
|
+
mock = _mock_reader()
|
|
99
|
+
reader._target = mock
|
|
100
|
+
reader._connected = True
|
|
101
|
+
nfc.transceive([0x30], tx_crc=False, rx_crc=False)
|
|
102
|
+
mock.transceive.assert_called_once_with([0x30], tx_crc=False, rx_crc=False, timeout=0.5)
|
|
103
|
+
|
|
104
|
+
def test_transceive_with_timeout(self):
|
|
105
|
+
mock = _mock_reader()
|
|
106
|
+
reader._target = mock
|
|
107
|
+
reader._connected = True
|
|
108
|
+
nfc.transceive([0x30], timeout=1.2)
|
|
109
|
+
mock.transceive.assert_called_once_with([0x30], tx_crc=True, rx_crc=True, timeout=1.2)
|
|
110
|
+
|
|
111
|
+
def test_transceive_empty_result(self):
|
|
112
|
+
mock = _mock_reader()
|
|
113
|
+
mock.transceive.return_value = None
|
|
114
|
+
reader._target = mock
|
|
115
|
+
reader._connected = True
|
|
116
|
+
result = nfc.transceive([0x30])
|
|
117
|
+
assert result == []
|
|
118
|
+
|
|
119
|
+
def test_transceive_not_connected(self, monkeypatch):
|
|
120
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
121
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
122
|
+
with pytest.raises(AssertionError):
|
|
123
|
+
nfc.transceive([0x30])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestTransceiveBits:
|
|
127
|
+
|
|
128
|
+
def test_transceive_bits_returns_result(self):
|
|
129
|
+
mock = _mock_reader()
|
|
130
|
+
reader._target = mock
|
|
131
|
+
reader._connected = True
|
|
132
|
+
result = nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
133
|
+
assert result is not None
|
|
134
|
+
assert result.data == [0xAA, 0xBB]
|
|
135
|
+
|
|
136
|
+
def test_transceive_bits_passes_params(self):
|
|
137
|
+
mock = _mock_reader()
|
|
138
|
+
reader._target = mock
|
|
139
|
+
reader._connected = True
|
|
140
|
+
nfc.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
141
|
+
mock.transceive.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False, timeout=0.5)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestReqa:
|
|
145
|
+
|
|
146
|
+
@patch("nfc.reader.transceive_bits")
|
|
147
|
+
def test_reqa_default(self, mock_tb):
|
|
148
|
+
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
149
|
+
result = nfc.reqa()
|
|
150
|
+
mock_tb.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
151
|
+
assert result is not None
|
|
152
|
+
|
|
153
|
+
@patch("nfc.reader.transceive_bits")
|
|
154
|
+
def test_reqa_custom_cmd(self, mock_tb):
|
|
155
|
+
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
156
|
+
nfc.reqa(cmd=0x20)
|
|
157
|
+
mock_tb.assert_called_once_with([0x20], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestWupa:
|
|
161
|
+
|
|
162
|
+
@patch("nfc.reader.transceive_bits")
|
|
163
|
+
def test_wupa(self, mock_tb):
|
|
164
|
+
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
165
|
+
result = nfc.wupa()
|
|
166
|
+
mock_tb.assert_called_once_with([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
167
|
+
assert result is not None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestHalt:
|
|
171
|
+
|
|
172
|
+
@patch("nfc.reader.transceive")
|
|
173
|
+
def test_halt(self, mock_ts):
|
|
174
|
+
mock_ts.return_value = []
|
|
175
|
+
result = nfc.halt()
|
|
176
|
+
mock_ts.assert_called_once_with([0x50, 0x00], tx_crc=True, rx_crc=True)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestSelect:
|
|
180
|
+
|
|
181
|
+
@patch("nfc.reader.transceive")
|
|
182
|
+
def test_select_cl1(self, mock_ts):
|
|
183
|
+
mock_ts.return_value = [0x08]
|
|
184
|
+
result = nfc.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
185
|
+
mock_ts.assert_called_once_with(
|
|
186
|
+
[0x93, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
187
|
+
tx_crc=True, rx_crc=True
|
|
188
|
+
)
|
|
189
|
+
assert result == [0x08]
|
|
190
|
+
|
|
191
|
+
@patch("nfc.reader.transceive")
|
|
192
|
+
def test_select_cl2(self, mock_ts):
|
|
193
|
+
mock_ts.return_value = [0x08]
|
|
194
|
+
nfc.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
195
|
+
mock_ts.assert_called_once_with(
|
|
196
|
+
[0x95, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
197
|
+
tx_crc=True, rx_crc=True
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@patch("nfc.reader.transceive")
|
|
201
|
+
def test_select_cl3(self, mock_ts):
|
|
202
|
+
mock_ts.return_value = [0x08]
|
|
203
|
+
nfc.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
204
|
+
mock_ts.assert_called_once_with(
|
|
205
|
+
[0x97, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
206
|
+
tx_crc=True, rx_crc=True
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def test_select_invalid_cl(self):
|
|
210
|
+
with pytest.raises(ValueError, match="Invalid CL level"):
|
|
211
|
+
nfc.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TestAnticoll:
|
|
215
|
+
|
|
216
|
+
@patch("nfc.reader.transceive_bits")
|
|
217
|
+
def test_anticoll_cl1(self, mock_tb):
|
|
218
|
+
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
219
|
+
result = nfc.anticoll(cl_level=1)
|
|
220
|
+
mock_tb.assert_called_once_with([0x93, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
221
|
+
assert result is not None
|
|
222
|
+
|
|
223
|
+
@patch("nfc.reader.transceive_bits")
|
|
224
|
+
def test_anticoll_cl2(self, mock_tb):
|
|
225
|
+
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
226
|
+
nfc.anticoll(cl_level=2)
|
|
227
|
+
mock_tb.assert_called_once_with([0x95, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
228
|
+
|
|
229
|
+
@patch("nfc.reader.transceive_bits")
|
|
230
|
+
def test_anticoll_with_uid_prefix(self, mock_tb):
|
|
231
|
+
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
232
|
+
nfc.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
|
|
233
|
+
mock_tb.assert_called_once_with(
|
|
234
|
+
[0x93, 0x20, 0x04, 0x12, 0x34, 0x56],
|
|
235
|
+
last_tx_bits=0, tx_crc=False, rx_crc=False
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class TestFieldControl:
|
|
240
|
+
|
|
241
|
+
def test_field_on(self):
|
|
242
|
+
mock = _mock_reader()
|
|
243
|
+
reader._target = mock
|
|
244
|
+
reader._connected = True
|
|
245
|
+
nfc.field_on()
|
|
246
|
+
assert mock.rf_field is True
|
|
247
|
+
|
|
248
|
+
def test_field_off(self):
|
|
249
|
+
mock = _mock_reader()
|
|
250
|
+
reader._target = mock
|
|
251
|
+
reader._connected = True
|
|
252
|
+
nfc.field_off()
|
|
253
|
+
assert mock.rf_field is False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestSession:
|
|
257
|
+
|
|
258
|
+
@patch("nfc.reader.disconnect")
|
|
259
|
+
@patch("nfc.reader.connect")
|
|
260
|
+
def test_session_yields_reader(self, mock_connect, mock_disconnect):
|
|
261
|
+
mock_rdr = _mock_reader()
|
|
262
|
+
|
|
263
|
+
def fake_connect(port="COM20", reader_type="pn532"):
|
|
264
|
+
reader._target = mock_rdr
|
|
265
|
+
reader._connected = True
|
|
266
|
+
|
|
267
|
+
mock_connect.side_effect = fake_connect
|
|
268
|
+
|
|
269
|
+
with nfc.session(port="COM99", reader_type="pn532") as r:
|
|
270
|
+
assert r is reader
|
|
271
|
+
mock_disconnect.assert_called_once()
|
|
272
|
+
|
|
273
|
+
def test_session_no_port_raises(self, monkeypatch):
|
|
274
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
275
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
276
|
+
with pytest.raises(ValueError, match="未指定串口"):
|
|
277
|
+
with nfc.session(reader_type="pn532"):
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
def test_session_no_reader_type_raises(self, monkeypatch):
|
|
281
|
+
monkeypatch.delenv("NFC_PORT", raising=False)
|
|
282
|
+
monkeypatch.delenv("NFC_READER", raising=False)
|
|
283
|
+
with pytest.raises(ValueError, match="未指定读卡器类型"):
|
|
284
|
+
with nfc.session(port="COM99"):
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class TestGetSelCmd:
|
|
289
|
+
|
|
290
|
+
def test_cl1(self):
|
|
291
|
+
from nfc.reader import _get_sel_cmd
|
|
292
|
+
assert _get_sel_cmd(1) == 0x93
|
|
293
|
+
|
|
294
|
+
def test_cl2(self):
|
|
295
|
+
from nfc.reader import _get_sel_cmd
|
|
296
|
+
assert _get_sel_cmd(2) == 0x95
|
|
297
|
+
|
|
298
|
+
def test_cl3(self):
|
|
299
|
+
from nfc.reader import _get_sel_cmd
|
|
300
|
+
assert _get_sel_cmd(3) == 0x97
|
|
301
|
+
|
|
302
|
+
def test_invalid(self):
|
|
303
|
+
from nfc.reader import _get_sel_cmd
|
|
304
|
+
with pytest.raises(ValueError):
|
|
305
|
+
_get_sel_cmd(4)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestReaderSingletonProxy:
|
|
309
|
+
|
|
310
|
+
@patch.object(CardReaderRegistry, "create")
|
|
311
|
+
def test_reader_rf_property_and_lazy_connect(self, mock_create, monkeypatch):
|
|
312
|
+
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
313
|
+
monkeypatch.setenv("NFC_READER", "pn532")
|
|
314
|
+
mock_rdr = _mock_reader()
|
|
315
|
+
mock_create.return_value = mock_rdr
|
|
316
|
+
|
|
317
|
+
# reader.rf triggers auto-connect
|
|
318
|
+
reader.rf = True
|
|
319
|
+
assert mock_rdr.rf_field is True
|
|
320
|
+
assert reader.rf is True
|
|
321
|
+
assert reader.rf_field is True
|
|
322
|
+
assert reader.mf_crypto is False
|
|
323
|
+
|
|
324
|
+
@patch.object(CardReaderRegistry, "create")
|
|
325
|
+
def test_reader_delegates_to_functions_and_target(self, mock_create, monkeypatch):
|
|
326
|
+
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
327
|
+
monkeypatch.setenv("NFC_READER", "pn532")
|
|
328
|
+
mock_rdr = _mock_reader()
|
|
329
|
+
mock_create.return_value = mock_rdr
|
|
330
|
+
|
|
331
|
+
# reader.active calls active()
|
|
332
|
+
card = reader.active()
|
|
333
|
+
assert card is not None
|
|
334
|
+
assert card.uid == [0x01, 0x02, 0x03, 0x04]
|
|
335
|
+
|
|
336
|
+
# reader.custom_method delegates to mock_rdr
|
|
337
|
+
mock_rdr.custom_method = MagicMock(return_value="OK")
|
|
338
|
+
assert reader.custom_method() == "OK"
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
"""reader 模块测试 (connect / get_reader / active / transceive / session 等)"""
|
|
2
|
-
import pytest
|
|
3
|
-
from unittest.mock import MagicMock, patch, PropertyMock
|
|
4
|
-
from nfc import reader
|
|
5
|
-
from nfctester.drivers.card_reader import CardInfo, TransceiveBits
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.fixture(autouse=True)
|
|
9
|
-
def reset_state():
|
|
10
|
-
"""每个测试前重置全局状态"""
|
|
11
|
-
reader._state._reader = None
|
|
12
|
-
reader._state._connected = False
|
|
13
|
-
yield
|
|
14
|
-
reader._state._reader = None
|
|
15
|
-
reader._state._connected = False
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _mock_reader():
|
|
19
|
-
mock = MagicMock()
|
|
20
|
-
mock.transceive.return_value = TransceiveBits(data=[0xAA, 0xBB], bits=8)
|
|
21
|
-
mock.active.return_value = CardInfo(uid=[0x01, 0x02, 0x03, 0x04], atq=[0x44, 0x00], sak=0x00)
|
|
22
|
-
return mock
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TestConnect:
|
|
26
|
-
|
|
27
|
-
@patch.object(reader.CardReaderRegistry, "create")
|
|
28
|
-
def test_connect_with_params(self, mock_create):
|
|
29
|
-
mock_reader = _mock_reader()
|
|
30
|
-
mock_create.return_value = mock_reader
|
|
31
|
-
reader.connect(port="COM99", reader_type="pn532")
|
|
32
|
-
mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
|
|
33
|
-
mock_reader.open.assert_called_once()
|
|
34
|
-
assert reader._state._connected is True
|
|
35
|
-
|
|
36
|
-
@patch.object(reader.CardReaderRegistry, "create")
|
|
37
|
-
def test_connect_from_env(self, mock_create, monkeypatch):
|
|
38
|
-
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
39
|
-
monkeypatch.setenv("NFC_READER", "pn532")
|
|
40
|
-
mock_create.return_value = _mock_reader()
|
|
41
|
-
reader.connect()
|
|
42
|
-
mock_create.assert_called_once_with("pn532", transport="serial", port="COM10")
|
|
43
|
-
|
|
44
|
-
def test_connect_no_port_raises(self):
|
|
45
|
-
with pytest.raises(ValueError, match="未指定串口"):
|
|
46
|
-
reader.connect(reader_type="pn532")
|
|
47
|
-
|
|
48
|
-
def test_connect_no_reader_type_raises(self):
|
|
49
|
-
with pytest.raises(ValueError, match="未指定读卡器类型"):
|
|
50
|
-
reader.connect(port="COM99")
|
|
51
|
-
|
|
52
|
-
@patch.object(reader.CardReaderRegistry, "create")
|
|
53
|
-
def test_connect_overrides_env(self, mock_create, monkeypatch):
|
|
54
|
-
monkeypatch.setenv("NFC_PORT", "COM10")
|
|
55
|
-
monkeypatch.setenv("NFC_READER", "clrc663")
|
|
56
|
-
mock_create.return_value = _mock_reader()
|
|
57
|
-
reader.connect(port="COM99", reader_type="pn532")
|
|
58
|
-
mock_create.assert_called_once_with("pn532", transport="serial", port="COM99")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class TestGetReader:
|
|
62
|
-
|
|
63
|
-
def test_get_reader_when_connected(self):
|
|
64
|
-
mock = _mock_reader()
|
|
65
|
-
reader._state._reader = mock
|
|
66
|
-
reader._state._connected = True
|
|
67
|
-
assert reader.get_reader() is mock
|
|
68
|
-
|
|
69
|
-
def test_get_reader_when_not_connected(self):
|
|
70
|
-
with pytest.raises(AssertionError):
|
|
71
|
-
reader.get_reader()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class TestTransceive:
|
|
75
|
-
|
|
76
|
-
def test_transceive_returns_data(self):
|
|
77
|
-
mock = _mock_reader()
|
|
78
|
-
reader._state._reader = mock
|
|
79
|
-
reader._state._connected = True
|
|
80
|
-
result = reader.transceive([0x30, 0x00])
|
|
81
|
-
assert result == [0xAA, 0xBB]
|
|
82
|
-
mock.transceive.assert_called_once_with([0x30, 0x00], tx_crc=True, rx_crc=True)
|
|
83
|
-
|
|
84
|
-
def test_transceive_no_crc(self):
|
|
85
|
-
mock = _mock_reader()
|
|
86
|
-
reader._state._reader = mock
|
|
87
|
-
reader._state._connected = True
|
|
88
|
-
reader.transceive([0x30], tx_crc=False, rx_crc=False)
|
|
89
|
-
mock.transceive.assert_called_once_with([0x30], tx_crc=False, rx_crc=False)
|
|
90
|
-
|
|
91
|
-
def test_transceive_empty_result(self):
|
|
92
|
-
mock = _mock_reader()
|
|
93
|
-
mock.transceive.return_value = None
|
|
94
|
-
reader._state._reader = mock
|
|
95
|
-
reader._state._connected = True
|
|
96
|
-
result = reader.transceive([0x30])
|
|
97
|
-
assert result == []
|
|
98
|
-
|
|
99
|
-
def test_transceive_not_connected(self):
|
|
100
|
-
with pytest.raises(AssertionError):
|
|
101
|
-
reader.transceive([0x30])
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class TestTransceiveBits:
|
|
105
|
-
|
|
106
|
-
def test_transceive_bits_returns_result(self):
|
|
107
|
-
mock = _mock_reader()
|
|
108
|
-
reader._state._reader = mock
|
|
109
|
-
reader._state._connected = True
|
|
110
|
-
result = reader.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
111
|
-
assert result is not None
|
|
112
|
-
assert result.data == [0xAA, 0xBB]
|
|
113
|
-
|
|
114
|
-
def test_transceive_bits_passes_params(self):
|
|
115
|
-
mock = _mock_reader()
|
|
116
|
-
reader._state._reader = mock
|
|
117
|
-
reader._state._connected = True
|
|
118
|
-
reader.transceive_bits([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
119
|
-
mock.transceive.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class TestReqa:
|
|
123
|
-
|
|
124
|
-
@patch.object(reader, "transceive_bits")
|
|
125
|
-
def test_reqa_default(self, mock_tb):
|
|
126
|
-
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
127
|
-
result = reader.reqa()
|
|
128
|
-
mock_tb.assert_called_once_with([0x26], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
129
|
-
assert result is not None
|
|
130
|
-
|
|
131
|
-
@patch.object(reader, "transceive_bits")
|
|
132
|
-
def test_reqa_custom_cmd(self, mock_tb):
|
|
133
|
-
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
134
|
-
reader.reqa(cmd=0x20)
|
|
135
|
-
mock_tb.assert_called_once_with([0x20], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
class TestWupa:
|
|
139
|
-
|
|
140
|
-
@patch.object(reader, "transceive_bits")
|
|
141
|
-
def test_wupa(self, mock_tb):
|
|
142
|
-
mock_tb.return_value = TransceiveBits(data=[0x44, 0x00], bits=8)
|
|
143
|
-
result = reader.wupa()
|
|
144
|
-
mock_tb.assert_called_once_with([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
145
|
-
assert result is not None
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class TestHalt:
|
|
149
|
-
|
|
150
|
-
@patch.object(reader, "transceive")
|
|
151
|
-
def test_halt(self, mock_ts):
|
|
152
|
-
mock_ts.return_value = []
|
|
153
|
-
result = reader.halt()
|
|
154
|
-
mock_ts.assert_called_once_with([0x50, 0x00], tx_crc=True, rx_crc=True)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class TestSelect:
|
|
158
|
-
|
|
159
|
-
@patch.object(reader, "transceive")
|
|
160
|
-
def test_select_cl1(self, mock_ts):
|
|
161
|
-
mock_ts.return_value = [0x08]
|
|
162
|
-
result = reader.select(cl_level=1, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
163
|
-
mock_ts.assert_called_once_with(
|
|
164
|
-
[0x93, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
165
|
-
tx_crc=True, rx_crc=True
|
|
166
|
-
)
|
|
167
|
-
assert result == [0x08]
|
|
168
|
-
|
|
169
|
-
@patch.object(reader, "transceive")
|
|
170
|
-
def test_select_cl2(self, mock_ts):
|
|
171
|
-
mock_ts.return_value = [0x08]
|
|
172
|
-
reader.select(cl_level=2, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
173
|
-
mock_ts.assert_called_once_with(
|
|
174
|
-
[0x95, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
175
|
-
tx_crc=True, rx_crc=True
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
@patch.object(reader, "transceive")
|
|
179
|
-
def test_select_cl3(self, mock_ts):
|
|
180
|
-
mock_ts.return_value = [0x08]
|
|
181
|
-
reader.select(cl_level=3, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
182
|
-
mock_ts.assert_called_once_with(
|
|
183
|
-
[0x97, 0x70, 0x04, 0x12, 0x34, 0x56, 0x61],
|
|
184
|
-
tx_crc=True, rx_crc=True
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
def test_select_invalid_cl(self):
|
|
188
|
-
with pytest.raises(ValueError, match="Invalid CL level"):
|
|
189
|
-
reader.select(cl_level=4, uid=[0x04, 0x12, 0x34, 0x56, 0x61])
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class TestAnticoll:
|
|
193
|
-
|
|
194
|
-
@patch.object(reader, "transceive_bits")
|
|
195
|
-
def test_anticoll_cl1(self, mock_tb):
|
|
196
|
-
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
197
|
-
result = reader.anticoll(cl_level=1)
|
|
198
|
-
mock_tb.assert_called_once_with([0x93, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
199
|
-
assert result is not None
|
|
200
|
-
|
|
201
|
-
@patch.object(reader, "transceive_bits")
|
|
202
|
-
def test_anticoll_cl2(self, mock_tb):
|
|
203
|
-
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
204
|
-
reader.anticoll(cl_level=2)
|
|
205
|
-
mock_tb.assert_called_once_with([0x95, 0x20], last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
206
|
-
|
|
207
|
-
@patch.object(reader, "transceive_bits")
|
|
208
|
-
def test_anticoll_with_uid_prefix(self, mock_tb):
|
|
209
|
-
mock_tb.return_value = TransceiveBits(data=[0x04, 0x12, 0x34, 0x56, 0x61], bits=40)
|
|
210
|
-
reader.anticoll(cl_level=1, uid_prefix=[0x04, 0x12, 0x34, 0x56])
|
|
211
|
-
mock_tb.assert_called_once_with(
|
|
212
|
-
[0x93, 0x20, 0x04, 0x12, 0x34, 0x56],
|
|
213
|
-
last_tx_bits=0, tx_crc=False, rx_crc=False
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
class TestFieldControl:
|
|
218
|
-
|
|
219
|
-
def test_field_on(self):
|
|
220
|
-
mock = _mock_reader()
|
|
221
|
-
reader._state._reader = mock
|
|
222
|
-
reader._state._connected = True
|
|
223
|
-
reader.field_on()
|
|
224
|
-
assert mock.rf_field is True
|
|
225
|
-
|
|
226
|
-
def test_field_off(self):
|
|
227
|
-
mock = _mock_reader()
|
|
228
|
-
reader._state._reader = mock
|
|
229
|
-
reader._state._connected = True
|
|
230
|
-
reader.field_off()
|
|
231
|
-
assert mock.rf_field is False
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class TestSession:
|
|
235
|
-
|
|
236
|
-
@patch.object(reader._state, "disconnect")
|
|
237
|
-
@patch.object(reader._state, "connect")
|
|
238
|
-
def test_session_yields_reader(self, mock_connect, mock_disconnect):
|
|
239
|
-
mock_rdr = _mock_reader()
|
|
240
|
-
|
|
241
|
-
def fake_connect(port="COM20", reader_type="pn532"):
|
|
242
|
-
reader._state._reader = mock_rdr
|
|
243
|
-
reader._state._connected = True
|
|
244
|
-
|
|
245
|
-
mock_connect.side_effect = fake_connect
|
|
246
|
-
|
|
247
|
-
with reader.session(port="COM99", reader_type="pn532") as r:
|
|
248
|
-
assert r is mock_rdr
|
|
249
|
-
mock_disconnect.assert_called_once()
|
|
250
|
-
|
|
251
|
-
def test_session_no_port_raises(self):
|
|
252
|
-
with pytest.raises(ValueError, match="未指定串口"):
|
|
253
|
-
with reader.session(reader_type="pn532"):
|
|
254
|
-
pass
|
|
255
|
-
|
|
256
|
-
def test_session_no_reader_type_raises(self):
|
|
257
|
-
with pytest.raises(ValueError, match="未指定读卡器类型"):
|
|
258
|
-
with reader.session(port="COM99"):
|
|
259
|
-
pass
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
class TestGetSelCmd:
|
|
263
|
-
|
|
264
|
-
def test_cl1(self):
|
|
265
|
-
assert reader._get_sel_cmd(1) == 0x93
|
|
266
|
-
|
|
267
|
-
def test_cl2(self):
|
|
268
|
-
assert reader._get_sel_cmd(2) == 0x95
|
|
269
|
-
|
|
270
|
-
def test_cl3(self):
|
|
271
|
-
assert reader._get_sel_cmd(3) == 0x97
|
|
272
|
-
|
|
273
|
-
def test_invalid(self):
|
|
274
|
-
with pytest.raises(ValueError):
|
|
275
|
-
reader._get_sel_cmd(4)
|
|
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
|