nfcscript 0.0.8__tar.gz → 0.0.10__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.8 → nfcscript-0.0.10}/PKG-INFO +12 -2
- {nfcscript-0.0.8 → nfcscript-0.0.10}/README.md +11 -1
- {nfcscript-0.0.8 → nfcscript-0.0.10}/pyproject.toml +2 -2
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/assertions.py +45 -0
- nfcscript-0.0.10/src/nfc/bits.py +42 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/checksum.py +10 -2
- nfcscript-0.0.10/src/nfc/delay.py +9 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/hex_util.py +48 -16
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/nfc_cli.py +50 -24
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/reader.py +85 -44
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfcscript.egg-info/PKG-INFO +12 -2
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfcscript.egg-info/SOURCES.txt +9 -1
- nfcscript-0.0.10/tests/test_assertions.py +123 -0
- nfcscript-0.0.10/tests/test_bits.py +69 -0
- nfcscript-0.0.10/tests/test_checksum.py +32 -0
- nfcscript-0.0.10/tests/test_cli.py +90 -0
- nfcscript-0.0.10/tests/test_cli_collect.py +105 -0
- nfcscript-0.0.10/tests/test_delay.py +22 -0
- nfcscript-0.0.10/tests/test_hex_util.py +168 -0
- nfcscript-0.0.10/tests/test_reader.py +275 -0
- nfcscript-0.0.10/tests/test_trace.py +73 -0
- nfcscript-0.0.8/src/nfc/bits.py +0 -28
- nfcscript-0.0.8/src/nfc/delay.py +0 -4
- nfcscript-0.0.8/tests/test_main.py +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/setup.cfg +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/__init__.py +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/cli.py +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfc/trace.py +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfcscript.egg-info/dependency_links.txt +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfcscript.egg-info/entry_points.txt +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfcscript.egg-info/requires.txt +0 -0
- {nfcscript-0.0.8 → nfcscript-0.0.10}/src/nfcscript.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nfcscript
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: A plugin-based core engine for DSL-style NFC testing automation.
|
|
5
5
|
Author: crthu
|
|
6
6
|
Requires-Python: >=3.14
|
|
@@ -167,7 +167,7 @@ with session() as reader:
|
|
|
167
167
|
|------|------|
|
|
168
168
|
| `connect(port, reader_type)` | 连接读卡器 |
|
|
169
169
|
| `get_reader()` | 获取当前读卡器实例 |
|
|
170
|
-
| `active(
|
|
170
|
+
| `active(low_layer, ignore_error, reqa_cmd)` | 寻卡,返回 uid/atq/sak |
|
|
171
171
|
| `transceive(data, tx_crc, rx_crc)` | 底层帧交互 |
|
|
172
172
|
| `transceive_bits(data, last_tx_bits, tx_crc, rx_crc)` | 位控制帧交互 |
|
|
173
173
|
| `reqa()` | ISO14443-A REQA |
|
|
@@ -253,6 +253,16 @@ NFC_TRACE_DRIVER=true
|
|
|
253
253
|
NFC_TRACE_PROTOCOL=true
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
+
## AI 编写脚本
|
|
257
|
+
|
|
258
|
+
本目录下 `SKILL.md` 提供了 DSL 的完整 API 参考和 few-shot 示例,可直接用于 AI Agent 生成 NFC 脚本。
|
|
259
|
+
|
|
260
|
+
**使用方式**:在 AI 对话中引用该文件,例如:
|
|
261
|
+
```
|
|
262
|
+
@nfcscript/SKILL.md
|
|
263
|
+
```
|
|
264
|
+
Agent 会基于 SKILL.md 中的 API 签名、few-shot 示例和约定生成正确的脚本代码。
|
|
265
|
+
|
|
256
266
|
## 开发
|
|
257
267
|
|
|
258
268
|
```bash
|
|
@@ -157,7 +157,7 @@ with session() as reader:
|
|
|
157
157
|
|------|------|
|
|
158
158
|
| `connect(port, reader_type)` | 连接读卡器 |
|
|
159
159
|
| `get_reader()` | 获取当前读卡器实例 |
|
|
160
|
-
| `active(
|
|
160
|
+
| `active(low_layer, ignore_error, reqa_cmd)` | 寻卡,返回 uid/atq/sak |
|
|
161
161
|
| `transceive(data, tx_crc, rx_crc)` | 底层帧交互 |
|
|
162
162
|
| `transceive_bits(data, last_tx_bits, tx_crc, rx_crc)` | 位控制帧交互 |
|
|
163
163
|
| `reqa()` | ISO14443-A REQA |
|
|
@@ -243,6 +243,16 @@ NFC_TRACE_DRIVER=true
|
|
|
243
243
|
NFC_TRACE_PROTOCOL=true
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
+
## AI 编写脚本
|
|
247
|
+
|
|
248
|
+
本目录下 `SKILL.md` 提供了 DSL 的完整 API 参考和 few-shot 示例,可直接用于 AI Agent 生成 NFC 脚本。
|
|
249
|
+
|
|
250
|
+
**使用方式**:在 AI 对话中引用该文件,例如:
|
|
251
|
+
```
|
|
252
|
+
@nfcscript/SKILL.md
|
|
253
|
+
```
|
|
254
|
+
Agent 会基于 SKILL.md 中的 API 签名、few-shot 示例和约定生成正确的脚本代码。
|
|
255
|
+
|
|
246
256
|
## 开发
|
|
247
257
|
|
|
248
258
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nfcscript"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.10"
|
|
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"
|
|
@@ -34,7 +34,7 @@ dev = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[tool.bumpversion]
|
|
37
|
-
current_version = "0.0.
|
|
37
|
+
current_version = "0.0.10"
|
|
38
38
|
commit = true
|
|
39
39
|
tag = true
|
|
40
40
|
allow_dirty = true
|
|
@@ -19,6 +19,17 @@ def _get_stack_trace() -> str:
|
|
|
19
19
|
return "\n".join(chain)
|
|
20
20
|
|
|
21
21
|
def ASSERT_LEN(data: Any, expected_len: int, msg: str | None = None) -> None:
|
|
22
|
+
"""
|
|
23
|
+
断言数据长度等于期望值。
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
data: 待检查的数据。
|
|
27
|
+
expected_len: 期望的长度。
|
|
28
|
+
msg: 失败时的自定义错误消息。
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
AssertionError: 长度不匹配时抛出。
|
|
32
|
+
"""
|
|
22
33
|
actual_len = len(data)
|
|
23
34
|
if actual_len != expected_len:
|
|
24
35
|
error_msg = (
|
|
@@ -33,6 +44,17 @@ def ASSERT_LEN(data: Any, expected_len: int, msg: str | None = None) -> None:
|
|
|
33
44
|
raise AssertionError(error_msg)
|
|
34
45
|
|
|
35
46
|
def ASSERT_EQUAL(expected: Any, actual: Any, msg: str | None = None) -> None:
|
|
47
|
+
"""
|
|
48
|
+
断言两个值相等。
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
expected: 期望值。
|
|
52
|
+
actual: 实际值。
|
|
53
|
+
msg: 失败时的自定义错误消息。
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
AssertionError: 值不相等时抛出。
|
|
57
|
+
"""
|
|
36
58
|
if actual != expected:
|
|
37
59
|
error_msg = (
|
|
38
60
|
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
@@ -45,6 +67,16 @@ def ASSERT_EQUAL(expected: Any, actual: Any, msg: str | None = None) -> None:
|
|
|
45
67
|
raise AssertionError(error_msg)
|
|
46
68
|
|
|
47
69
|
def ASSERT_IS_NONE(value: Any, msg: str | None = None) -> None:
|
|
70
|
+
"""
|
|
71
|
+
断言值为 None。
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
value: 待检查的值。
|
|
75
|
+
msg: 失败时的自定义错误消息。
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
AssertionError: 值不为 None 时抛出。
|
|
79
|
+
"""
|
|
48
80
|
if value is not None:
|
|
49
81
|
error_msg = (
|
|
50
82
|
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
@@ -56,6 +88,19 @@ def ASSERT_IS_NONE(value: Any, msg: str | None = None) -> None:
|
|
|
56
88
|
raise AssertionError(error_msg)
|
|
57
89
|
|
|
58
90
|
def ASSERT_IS_NOT_NONE(value: T | None, msg: str | None = None) -> TypeGuard[T]:
|
|
91
|
+
"""
|
|
92
|
+
断言值不为 None。
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
value: 待检查的值。
|
|
96
|
+
msg: 失败时的自定义错误消息。
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
TypeGuard[T]: 值不为 None 时返回 True。
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
AssertionError: 值为 None 时抛出。
|
|
103
|
+
"""
|
|
59
104
|
if value is None:
|
|
60
105
|
error_msg = (
|
|
61
106
|
f"\nAssertion Failed: {msg}\n" if msg else ""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .hex_util import FORMAT_HEX
|
|
2
|
+
|
|
3
|
+
def BITS_UPDATE(val: int, mask: int, data: int) -> int:
|
|
4
|
+
"""
|
|
5
|
+
更新寄存器中的特定位域 (Read-Modify-Write)。
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
val: 当前寄存器的原始值。
|
|
9
|
+
mask: 预移位的位掩码 (例如: 0x20)。
|
|
10
|
+
data: 已移位对齐的新数值 (例如: 0x20, 即 1 << 5)。
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
int: 更新后的寄存器值。
|
|
14
|
+
"""
|
|
15
|
+
assert (data & ~mask) == 0, f"Data {FORMAT_HEX(data)} contains bits outside mask {FORMAT_HEX(mask)}"
|
|
16
|
+
return (val & ~mask) | (data & mask)
|
|
17
|
+
|
|
18
|
+
def BITS_SET(val: int, mask: int) -> int:
|
|
19
|
+
"""
|
|
20
|
+
将寄存器中的指定位强制置 1 (OR 操作)。
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
val: 当前寄存器的原始值。
|
|
24
|
+
mask: 想要置 1 的位掩码。
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
int: 置位后的寄存器值。
|
|
28
|
+
"""
|
|
29
|
+
return val | mask
|
|
30
|
+
|
|
31
|
+
def BITS_RESET(val: int, mask: int) -> int:
|
|
32
|
+
"""
|
|
33
|
+
将寄存器中的指定位强制清 0 (AND-NOT 操作)。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
val: 当前寄存器的原始值。
|
|
37
|
+
mask: 想要清 0 的位掩码。
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
int: 清位后的寄存器值。
|
|
41
|
+
"""
|
|
42
|
+
return val & ~mask
|
|
@@ -3,8 +3,16 @@ from .hex_util import FORMAT_HEX
|
|
|
3
3
|
|
|
4
4
|
def GET_BCC(data: list[int]) -> int:
|
|
5
5
|
"""
|
|
6
|
-
计算 BCC: data[0]^data[1]^data[2]^data[3]
|
|
7
|
-
|
|
6
|
+
计算 BCC: data[0]^data[1]^data[2]^data[3]。
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
data: 4 字节的 UID 数据。
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
int: 异或校验值。
|
|
13
|
+
|
|
14
|
+
Raises:
|
|
15
|
+
AssertionError: 输入数据长度不为 4 时抛出。
|
|
8
16
|
"""
|
|
9
17
|
ASSERT_LEN(data, 4, msg=f"计算BCC时输入数据长度应为4: {FORMAT_HEX(data)}")
|
|
10
18
|
return data[0] ^ data[1] ^ data[2] ^ data[3]
|
|
@@ -5,25 +5,43 @@ BIT_PATTERN = re.compile(r"(\d+)\s*\'h\s*([0-9a-fA-F]+)")
|
|
|
5
5
|
|
|
6
6
|
def PARSE_HEX(text: str) -> tuple[str, int]:
|
|
7
7
|
"""
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
解析带位标记的 hex 字符串。
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
text: 十六进制文本。
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
tuple[str, int]: (纯十六进制字符串, 最后字节有效位数)。
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
ValueError: 不合法的十六进制数据时抛出。
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
>>> PARSE_HEX("AA BB 7'h03")
|
|
21
|
+
('AABB03', 7)
|
|
22
|
+
>>> PARSE_HEX("FF 4'h0C")
|
|
23
|
+
('FF0C', 4)
|
|
24
|
+
>>> PARSE_HEX("AABBCC")
|
|
25
|
+
('AABBCC', 0)
|
|
10
26
|
"""
|
|
11
27
|
last_bits = 0
|
|
12
28
|
cleaned = text.strip()
|
|
13
29
|
|
|
14
|
-
#
|
|
30
|
+
# 查找所有位标记,从右往左替换避免偏移
|
|
15
31
|
matches = list(BIT_PATTERN.finditer(cleaned))
|
|
16
32
|
if matches:
|
|
17
33
|
last_match = matches[-1]
|
|
18
34
|
last_bits = int(last_match.group(1))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
|
|
36
|
+
for m in reversed(matches):
|
|
37
|
+
hex_val = m.group(2)
|
|
38
|
+
if len(hex_val) % 2 != 0:
|
|
39
|
+
hex_val = "0" + hex_val
|
|
40
|
+
cleaned = cleaned[:m.start()] + hex_val + cleaned[m.end():]
|
|
23
41
|
|
|
24
42
|
# 去空格得到纯 hex
|
|
25
43
|
clean_hex = cleaned.replace(" ", "")
|
|
26
|
-
|
|
44
|
+
|
|
27
45
|
# 简单的合法性检查
|
|
28
46
|
if clean_hex:
|
|
29
47
|
try:
|
|
@@ -35,17 +53,31 @@ def PARSE_HEX(text: str) -> tuple[str, int]:
|
|
|
35
53
|
|
|
36
54
|
def FORMAT_HEX(data: int | bytes | list[int] | str, last_bits: int = 0) -> str:
|
|
37
55
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
格式化数据为带位标记的可视化格式。
|
|
57
|
+
|
|
58
|
+
支持输入类型: int, bytes, list[int], str (纯 hex)。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data: 待格式化的数据。
|
|
62
|
+
last_bits: 最后一个字节的有效位数 (1-7),0 表示整字节。
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: 格式化后的字符串。
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
TypeError: 不支持的输入类型时抛出。
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
>>> FORMAT_HEX(0xAABB03, 7)
|
|
72
|
+
"AA BB 7'h03"
|
|
73
|
+
>>> FORMAT_HEX([0xAA, 0xBB, 0x03])
|
|
74
|
+
'AA BB 03'
|
|
75
|
+
>>> FORMAT_HEX("AABB", 4)
|
|
76
|
+
"AA 4'hBB"
|
|
45
77
|
"""
|
|
46
78
|
# 1. 统一转换为纯十六进制字符串
|
|
47
79
|
if isinstance(data, int):
|
|
48
|
-
hex_str =
|
|
80
|
+
hex_str = data.to_bytes(max(1, (data.bit_length() + 7) // 8)).hex().upper()
|
|
49
81
|
elif isinstance(data, bytes):
|
|
50
82
|
hex_str = data.hex().upper()
|
|
51
83
|
elif isinstance(data, list):
|
|
@@ -8,6 +8,25 @@ from nfc import *
|
|
|
8
8
|
from nfctester.registry import CardRegistry
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def cli_collect(**lengths):
|
|
12
|
+
"""
|
|
13
|
+
标记方法参数为 list 收集模式。
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
@cli_collect(key=6, uid=4) # key 固定 6,uid 固定 4,剩余自动收集
|
|
17
|
+
@cli_collect(data=6) # data 固定 6
|
|
18
|
+
@cli_collect() # 回退到最后一个参数收集剩余
|
|
19
|
+
|
|
20
|
+
CLI 调用示例:
|
|
21
|
+
mf_auth 4 0 01 02 03 04 05 06 07 08 09 10
|
|
22
|
+
# → block=4, key_type=0, key=[1..6], uid=[7..10]
|
|
23
|
+
"""
|
|
24
|
+
def decorator(func):
|
|
25
|
+
func._cli_collect = lengths
|
|
26
|
+
return func
|
|
27
|
+
return decorator
|
|
28
|
+
|
|
29
|
+
|
|
11
30
|
def _load_external_cards(paths):
|
|
12
31
|
"""加载外部卡片模块,触发 @CardRegistry.register() 注册"""
|
|
13
32
|
for path in paths:
|
|
@@ -82,36 +101,43 @@ def _call_method(method, args_str):
|
|
|
82
101
|
params = list(sig.parameters.keys())
|
|
83
102
|
|
|
84
103
|
if not args_str:
|
|
85
|
-
# 无参数调用
|
|
86
104
|
return method()
|
|
87
105
|
|
|
88
|
-
|
|
89
|
-
raw_args = args_str.split()
|
|
90
|
-
parsed = []
|
|
91
|
-
for i, raw in enumerate(raw_args):
|
|
92
|
-
parsed.append(_parse_value(raw))
|
|
106
|
+
parsed = [_parse_value(raw) for raw in args_str.split()]
|
|
93
107
|
|
|
94
108
|
# 跳过 self
|
|
95
109
|
if params and params[0] == "self":
|
|
96
110
|
params = params[1:]
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
final_args.append(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
cli_collect_meta = getattr(method, '_cli_collect', None)
|
|
113
|
+
|
|
114
|
+
if cli_collect_meta:
|
|
115
|
+
# 装饰器模式:标记的按长度取,未标记的取单个值
|
|
116
|
+
# 无长度标记 (None) 的 list 参数收集剩余
|
|
117
|
+
final_args = []
|
|
118
|
+
i = 0
|
|
119
|
+
for param_name in params:
|
|
120
|
+
if param_name in cli_collect_meta:
|
|
121
|
+
length = cli_collect_meta[param_name]
|
|
122
|
+
if length is not None:
|
|
123
|
+
final_args.append(parsed[i:i + length])
|
|
124
|
+
i += length
|
|
125
|
+
else:
|
|
126
|
+
# 不定长,收集剩余
|
|
127
|
+
final_args.append(parsed[i:])
|
|
128
|
+
break
|
|
129
|
+
else:
|
|
130
|
+
final_args.append(parsed[i])
|
|
131
|
+
i += 1
|
|
132
|
+
else:
|
|
133
|
+
# 回退模式:最后一个参数收集多余值为 list
|
|
134
|
+
if len(parsed) <= len(params):
|
|
135
|
+
# 参数刚好或不够,直接传
|
|
136
|
+
final_args = parsed
|
|
137
|
+
else:
|
|
138
|
+
# 有剩余,最后一个参数收集多余值
|
|
139
|
+
final_args = parsed[:len(params) - 1]
|
|
140
|
+
final_args.append(parsed[len(params) - 1:])
|
|
115
141
|
|
|
116
142
|
return method(*final_args)
|
|
117
143
|
|
|
@@ -179,7 +205,7 @@ def main():
|
|
|
179
205
|
|
|
180
206
|
result = _call_method(methods[cmd_name], args_str)
|
|
181
207
|
if result is not None:
|
|
182
|
-
if isinstance(result,
|
|
208
|
+
if isinstance(result, list):
|
|
183
209
|
print(f"→ {FORMAT_HEX(result)}")
|
|
184
210
|
elif isinstance(result, bool):
|
|
185
211
|
print(f"→ {'OK' if result else 'FAIL'}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from contextlib import contextmanager
|
|
3
|
-
from nfctester.drivers.card_reader import CardInfo,
|
|
3
|
+
from nfctester.drivers.card_reader import CardInfo, TransceiveBits
|
|
4
4
|
from nfctester.registry import CardReaderRegistry
|
|
5
5
|
from .assertions import ASSERT_EQUAL, ASSERT_IS_NOT_NONE, ASSERT_LEN
|
|
6
6
|
from .checksum import GET_BCC
|
|
@@ -76,28 +76,27 @@ def get_reader():
|
|
|
76
76
|
return _state.reader
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def active(
|
|
79
|
+
def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -> CardInfo | None:
|
|
80
80
|
"""
|
|
81
81
|
寻卡操作,检测并激活目标卡片。
|
|
82
82
|
|
|
83
83
|
Args:
|
|
84
|
-
|
|
85
|
-
如果为 False
|
|
84
|
+
low_layer: 如果为 True,由 nfcscript 执行底层抗冲突流程以兼容非标卡;
|
|
85
|
+
如果为 False,则使用读卡器驱动层原生 active()。
|
|
86
86
|
ignore_error: 如果为 True,当 BCC 或 SAK 校验失败时,记录警告而不停止执行。
|
|
87
|
-
reqa_cmd: 底层寻卡时使用的 REQA 命令字节,默认 0x26
|
|
88
|
-
仅在 ll=True 时生效。
|
|
87
|
+
reqa_cmd: 底层寻卡时使用的 REQA 命令字节,默认 0x26。仅 low_layer=True 时生效。
|
|
89
88
|
|
|
90
89
|
Returns:
|
|
91
90
|
CardInfo: 包含卡片信息的数据类,失败时返回 None。
|
|
92
91
|
属性说明:
|
|
93
|
-
- 'uid' (
|
|
94
|
-
- 'atq' (
|
|
92
|
+
- 'uid' (list[int]): 卡片的 UID。
|
|
93
|
+
- 'atq' (list[int]): 卡片的 ATQ (Answer To Request)。
|
|
95
94
|
- 'sak' (int): 卡片的 SAK (Select Acknowledge)。
|
|
96
95
|
"""
|
|
97
96
|
_state.ensure_connected()
|
|
98
97
|
reader = _state.reader
|
|
99
98
|
|
|
100
|
-
if not
|
|
99
|
+
if not low_layer:
|
|
101
100
|
return reader.active()
|
|
102
101
|
|
|
103
102
|
# 手动底层寻卡流程
|
|
@@ -129,7 +128,7 @@ def active(ll: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -
|
|
|
129
128
|
|
|
130
129
|
print(f"cl{cl}.uid: {FORMAT_HEX(data)}")
|
|
131
130
|
has_next = (data[0] == 0x88)
|
|
132
|
-
uid_to_select =
|
|
131
|
+
uid_to_select = data[0:5]
|
|
133
132
|
sak_res = select(cl_level=cl, uid=uid_to_select)
|
|
134
133
|
|
|
135
134
|
# SAK 校验
|
|
@@ -147,7 +146,7 @@ def active(ll: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -
|
|
|
147
146
|
sak = sak_res[0] if sak_res else 0
|
|
148
147
|
break
|
|
149
148
|
|
|
150
|
-
return CardInfo(uid=
|
|
149
|
+
return CardInfo(uid=full_uid, atq=atq, sak=sak)
|
|
151
150
|
|
|
152
151
|
|
|
153
152
|
def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> list[int]:
|
|
@@ -156,95 +155,137 @@ def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> lis
|
|
|
156
155
|
|
|
157
156
|
Args:
|
|
158
157
|
data: 要发送的字节列表。
|
|
159
|
-
tx_crc: 发送时是否自动添加 CRC。
|
|
160
|
-
rx_crc: 接收时是否自动校验 CRC。
|
|
158
|
+
tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
|
|
159
|
+
rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
|
|
161
160
|
|
|
162
161
|
Returns:
|
|
163
162
|
list[int]: 接收到的数据字节列表。
|
|
164
163
|
"""
|
|
165
164
|
_state.ensure_connected()
|
|
166
165
|
reader = _state.reader
|
|
167
|
-
res = reader.transceive(
|
|
168
|
-
return
|
|
166
|
+
res = reader.transceive(data, tx_crc=tx_crc, rx_crc=rx_crc)
|
|
167
|
+
return res.data if res and res.data else []
|
|
169
168
|
|
|
170
169
|
|
|
171
|
-
def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) ->
|
|
170
|
+
def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> TransceiveBits | None:
|
|
172
171
|
"""
|
|
173
172
|
使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
|
|
174
173
|
|
|
175
174
|
Args:
|
|
176
175
|
data: 要发送的字节列表。
|
|
177
|
-
last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
rx_crc: 接收时是否自动校验 CRC。
|
|
176
|
+
last_tx_bits: 最后一个字节实际发送的有效位数 (1-7),0 表示发送完整字节。
|
|
177
|
+
tx_crc: 发送时是否自动添加 CRC。False 则原样发送。
|
|
178
|
+
rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
|
|
181
179
|
|
|
182
180
|
Returns:
|
|
183
|
-
|
|
181
|
+
TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
|
|
184
182
|
"""
|
|
185
183
|
_state.ensure_connected()
|
|
186
184
|
reader = _state.reader
|
|
187
|
-
return reader.
|
|
185
|
+
return reader.transceive(data, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
|
|
188
186
|
|
|
189
187
|
|
|
190
|
-
def reqa(cmd: int = 0x26) ->
|
|
191
|
-
"""
|
|
192
|
-
|
|
188
|
+
def reqa(cmd: int = 0x26) -> TransceiveBits | None:
|
|
189
|
+
"""
|
|
190
|
+
ISO14443-A REQA (7 bits)。
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
cmd: REQA 命令字节,默认 0x26。
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
|
|
197
|
+
"""
|
|
193
198
|
return transceive_bits([cmd], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
194
199
|
|
|
195
200
|
|
|
196
|
-
def wupa() ->
|
|
197
|
-
"""
|
|
198
|
-
|
|
201
|
+
def wupa() -> TransceiveBits | None:
|
|
202
|
+
"""
|
|
203
|
+
ISO14443-A WUPA (7 bits)。
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
|
|
207
|
+
"""
|
|
199
208
|
return transceive_bits([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
|
|
200
209
|
|
|
201
210
|
|
|
202
211
|
def halt() -> list[int] | None:
|
|
203
|
-
"""
|
|
204
|
-
|
|
212
|
+
"""
|
|
213
|
+
ISO14443-A HALT (Standard Frame)。
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
list[int] | None: 返回响应数据,失败返回 None。
|
|
217
|
+
"""
|
|
205
218
|
return transceive([0x50, 0x00], tx_crc=True, rx_crc=True)
|
|
206
219
|
|
|
207
220
|
|
|
208
221
|
def _get_sel_cmd(cl_level: int) -> int:
|
|
209
|
-
"""
|
|
222
|
+
"""
|
|
223
|
+
根据 CL 级别获取 ISO14443-A SEL 命令字节。
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
cl_level: CL 级别,1/2/3。
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
int: 对应的 SEL 命令字节 (0x93/0x95/0x97)。
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ValueError: CL 级别不在 1/2/3 范围内。
|
|
233
|
+
"""
|
|
210
234
|
sel_map = {1: 0x93, 2: 0x95, 3: 0x97}
|
|
211
235
|
if cl_level not in sel_map:
|
|
212
236
|
raise ValueError(f"Invalid CL level: {cl_level}, must be 1, 2, or 3")
|
|
213
237
|
return sel_map[cl_level]
|
|
214
238
|
|
|
215
239
|
|
|
216
|
-
def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) ->
|
|
240
|
+
def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) -> TransceiveBits | None:
|
|
217
241
|
"""
|
|
218
|
-
ISO14443-A ANTICOLL (Anti-collision)
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
ISO14443-A ANTICOLL (Anti-collision)。
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
cl_level: CL 级别,1 (0x93), 2 (0x95), 或 3 (0x97)。
|
|
246
|
+
nvb: Number of Valid Bits,定义了包含 SEL 和 NVB 在内的有效位数。
|
|
247
|
+
uid_prefix: 已知的部分 UID (0-4 字节)。
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
|
|
223
251
|
"""
|
|
224
252
|
cmd = [_get_sel_cmd(cl_level), nvb] + uid_prefix
|
|
225
|
-
# 抗冲突响应无 CRC
|
|
226
253
|
return transceive_bits(cmd, last_tx_bits=0, tx_crc=False, rx_crc=False)
|
|
227
254
|
|
|
228
255
|
|
|
229
256
|
def select(cl_level: int, uid: list[int]) -> list[int] | None:
|
|
230
257
|
"""
|
|
231
|
-
ISO14443-A SELECT (Standard Frame)
|
|
232
|
-
|
|
233
|
-
:
|
|
258
|
+
ISO14443-A SELECT (Standard Frame)。
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
cl_level: CL 级别,1 (0x93), 2 (0x95), 或 3 (0x97)。
|
|
262
|
+
uid: 完整 5 字节 UID (包含 BCC)。
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
list[int] | None: 返回响应数据,失败返回 None。
|
|
234
266
|
"""
|
|
235
267
|
cmd = [_get_sel_cmd(cl_level), 0x70] + uid
|
|
236
|
-
# 选择帧需带 CRC
|
|
237
268
|
return transceive(cmd, tx_crc=True, rx_crc=True)
|
|
238
269
|
|
|
239
270
|
|
|
240
271
|
def field_on():
|
|
241
|
-
"""
|
|
272
|
+
"""
|
|
273
|
+
开启 PN532 的 RF 场。
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
AssertionError: 未连接读卡器时抛出。
|
|
277
|
+
"""
|
|
242
278
|
_state.ensure_connected()
|
|
243
279
|
_state.reader.rf_field = True
|
|
244
280
|
|
|
245
281
|
|
|
246
282
|
def field_off():
|
|
247
|
-
"""
|
|
283
|
+
"""
|
|
284
|
+
关闭 PN532 的 RF 场。
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
AssertionError: 未连接读卡器时抛出。
|
|
288
|
+
"""
|
|
248
289
|
_state.ensure_connected()
|
|
249
290
|
_state.reader.rf_field = False
|
|
250
291
|
|