nfcscript 0.0.9__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.
Files changed (32) hide show
  1. {nfcscript-0.0.9 → nfcscript-0.0.10}/PKG-INFO +3 -3
  2. {nfcscript-0.0.9 → nfcscript-0.0.10}/README.md +2 -2
  3. {nfcscript-0.0.9 → nfcscript-0.0.10}/pyproject.toml +2 -2
  4. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/assertions.py +45 -0
  5. nfcscript-0.0.10/src/nfc/bits.py +42 -0
  6. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/checksum.py +10 -2
  7. nfcscript-0.0.10/src/nfc/delay.py +9 -0
  8. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/hex_util.py +48 -16
  9. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/nfc_cli.py +50 -24
  10. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/reader.py +85 -44
  11. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfcscript.egg-info/PKG-INFO +3 -3
  12. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfcscript.egg-info/SOURCES.txt +9 -1
  13. nfcscript-0.0.10/tests/test_assertions.py +123 -0
  14. nfcscript-0.0.10/tests/test_bits.py +69 -0
  15. nfcscript-0.0.10/tests/test_checksum.py +32 -0
  16. nfcscript-0.0.10/tests/test_cli.py +90 -0
  17. nfcscript-0.0.10/tests/test_cli_collect.py +105 -0
  18. nfcscript-0.0.10/tests/test_delay.py +22 -0
  19. nfcscript-0.0.10/tests/test_hex_util.py +168 -0
  20. nfcscript-0.0.10/tests/test_reader.py +275 -0
  21. nfcscript-0.0.10/tests/test_trace.py +73 -0
  22. nfcscript-0.0.9/src/nfc/bits.py +0 -28
  23. nfcscript-0.0.9/src/nfc/delay.py +0 -4
  24. nfcscript-0.0.9/tests/test_main.py +0 -0
  25. {nfcscript-0.0.9 → nfcscript-0.0.10}/setup.cfg +0 -0
  26. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/__init__.py +0 -0
  27. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/cli.py +0 -0
  28. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfc/trace.py +0 -0
  29. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfcscript.egg-info/dependency_links.txt +0 -0
  30. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfcscript.egg-info/entry_points.txt +0 -0
  31. {nfcscript-0.0.9 → nfcscript-0.0.10}/src/nfcscript.egg-info/requires.txt +0 -0
  32. {nfcscript-0.0.9 → 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.9
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(ll, ignore_error)` | 寻卡,返回 uid/atq/sak |
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 |
@@ -191,7 +191,7 @@ with session() as reader:
191
191
 
192
192
  | 函数 | 说明 |
193
193
  |------|------|
194
- | `ASSERT_EQUAL(actual, expected, msg)` | 相等断言 |
194
+ | `ASSERT_EQUAL(expected, actual, msg)` | 相等断言 |
195
195
  | `ASSERT_LEN(data, expected_len, msg)` | 长度断言 |
196
196
  | `ASSERT_IS_NOT_NONE(value, msg)` | 非空断言 |
197
197
  | `ASSERT_IS_NONE(value, msg)` | 空值断言 |
@@ -157,7 +157,7 @@ with session() as reader:
157
157
  |------|------|
158
158
  | `connect(port, reader_type)` | 连接读卡器 |
159
159
  | `get_reader()` | 获取当前读卡器实例 |
160
- | `active(ll, ignore_error)` | 寻卡,返回 uid/atq/sak |
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 |
@@ -181,7 +181,7 @@ with session() as reader:
181
181
 
182
182
  | 函数 | 说明 |
183
183
  |------|------|
184
- | `ASSERT_EQUAL(actual, expected, msg)` | 相等断言 |
184
+ | `ASSERT_EQUAL(expected, actual, msg)` | 相等断言 |
185
185
  | `ASSERT_LEN(data, expected_len, msg)` | 长度断言 |
186
186
  | `ASSERT_IS_NOT_NONE(value, msg)` | 非空断言 |
187
187
  | `ASSERT_IS_NONE(value, msg)` | 空值断言 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nfcscript"
3
- version = "0.0.9"
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.9"
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
- :param data: 4字节的UID数据
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]
@@ -0,0 +1,9 @@
1
+ import time
2
+
3
+ def DELAY_MS(ms: int) -> None:
4
+ """毫秒级延时。
5
+
6
+ Args:
7
+ ms: 延时毫秒数。
8
+ """
9
+ time.sleep(ms / 1000.0)
@@ -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
- 支持: "AA BB 7'h03" -> ("AABB03", 7)
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
- hex_val = last_match.group(2)
20
-
21
- # 替换位标记为纯 hex
22
- cleaned = cleaned[:last_match.start()] + hex_val + cleaned[last_match.end():]
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
- 支持输入类型: int, bytes, list[int], str (纯 hex)
40
- 示例:
41
- FORMAT_HEX(0xAABB03, 7) -> "AA BB 7'h03"
42
- FORMAT_HEX("AABB03", 7) -> "AA BB 7'h03"
43
- FORMAT_HEX(b'\xaa\xbb\x03', 7) -> "AA BB 7'h03"
44
- FORMAT_HEX([0xAA, 0xBB, 0x03], 7) -> "AA BB 7'h03"
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 = hex(data)[2:].upper()
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
- # 处理 bytes 参数:将多个连续值合并为 bytes
99
- final_args = []
100
- i = 0
101
- while i < len(parsed):
102
- if i < len(params):
103
- param = sig.parameters[params[i] if params[0] != "self" else params[i]]
104
- annotation = param.annotation
105
- if annotation is bytes or (isinstance(annotation, type) and issubclass(annotation, bytes)):
106
- # 收集剩余参数作为 bytes
107
- remaining = [x for x in parsed[i:] if isinstance(x, (int, bytes))]
108
- if remaining and all(isinstance(x, int) for x in remaining):
109
- final_args.append(bytes(remaining))
110
- elif remaining:
111
- final_args.append(b"".join(x if isinstance(x, bytes) else bytes([x]) for x in remaining))
112
- break
113
- final_args.append(parsed[i])
114
- i += 1
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, bytes):
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, TransceiveResult
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(ll: bool = False, ignore_error: bool = False, reqa_cmd: int = 0x26) -> CardInfo | None:
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
- ll: 如果为 True,则执行底层抗冲突流程以兼容非标卡;
85
- 如果为 False,则使用读卡器原生 active()。
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' (bytes): 卡片的 UID。
94
- - 'atq' (bytes): 卡片的 ATQ (Answer To Request)。
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 ll:
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 = list(data[0:5])
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=bytes(full_uid), atq=bytes(atq), sak=sak)
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(bytes(data), tx_crc=tx_crc, rx_crc=rx_crc)
168
- return list(res.data) if res and res.data else []
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) -> TransceiveResult | None:
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
- 0 表示发送完整字节。
179
- tx_crc: 发送时是否自动添加 CRC。
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
- TransceiveResult | None: 包含 data 和 rx_bits 属性,失败返回 None。
181
+ TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
184
182
  """
185
183
  _state.ensure_connected()
186
184
  reader = _state.reader
187
- return reader.transceive_bits(bytes(data), last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
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) -> TransceiveResult | None:
191
- """ISO14443-A REQA (7 bits)"""
192
- # REQA: 0x26,短帧,仅发送 7 bits,响应无 CRC
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() -> TransceiveResult | None:
197
- """ISO14443-A WUPA (7 bits)"""
198
- # WUPA: 0x52,短帧,仅发送 7 bits,响应无 CRC
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
- """ISO14443-A HALT (Standard Frame)"""
204
- # HALT: 0x50 0x00 + CRC
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
- """根据 CL 级别获取 ISO14443-A SEL 命令字节"""
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] = []) -> TransceiveResult | None:
240
+ def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) -> TransceiveBits | None:
217
241
  """
218
- ISO14443-A ANTICOLL (Anti-collision)
219
- :param cl_level: 1 (0x93), 2 (0x95), or 3 (0x97)
220
- :param nvb: Number of Valid Bits,定义了包含 SEL 和 NVB 在内的有效位数
221
- :param uid_prefix: 已知的部分 UID (0-4 字节)
222
- :return: TransceiveResult 包含 datarx_bits 属性
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
- :param cl_level: 1 (0x93), 2 (0x95), or 3 (0x97)
233
- :param uid: 完整 5 字节 UID (包含 BCC)
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
- """开启 PN532 的 RF 场。"""
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
- """关闭 PN532 的 RF 场。"""
283
+ """
284
+ 关闭 PN532 的 RF 场。
285
+
286
+ Raises:
287
+ AssertionError: 未连接读卡器时抛出。
288
+ """
248
289
  _state.ensure_connected()
249
290
  _state.reader.rf_field = False
250
291
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.9
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(ll, ignore_error)` | 寻卡,返回 uid/atq/sak |
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 |
@@ -191,7 +191,7 @@ with session() as reader:
191
191
 
192
192
  | 函数 | 说明 |
193
193
  |------|------|
194
- | `ASSERT_EQUAL(actual, expected, msg)` | 相等断言 |
194
+ | `ASSERT_EQUAL(expected, actual, msg)` | 相等断言 |
195
195
  | `ASSERT_LEN(data, expected_len, msg)` | 长度断言 |
196
196
  | `ASSERT_IS_NOT_NONE(value, msg)` | 非空断言 |
197
197
  | `ASSERT_IS_NONE(value, msg)` | 空值断言 |
@@ -16,4 +16,12 @@ src/nfcscript.egg-info/dependency_links.txt
16
16
  src/nfcscript.egg-info/entry_points.txt
17
17
  src/nfcscript.egg-info/requires.txt
18
18
  src/nfcscript.egg-info/top_level.txt
19
- tests/test_main.py
19
+ tests/test_assertions.py
20
+ tests/test_bits.py
21
+ tests/test_checksum.py
22
+ tests/test_cli.py
23
+ tests/test_cli_collect.py
24
+ tests/test_delay.py
25
+ tests/test_hex_util.py
26
+ tests/test_reader.py
27
+ tests/test_trace.py