Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

582 rader
22 KiB

  1. #!/usr/bin/env python3
  2. """Phase-based PLSR HSD/SFD persistence board acceptance tool.
  3. Normal phases never corrupt storage and issue at most one SFD SAVE per
  4. invocation. Power removal/reset is intentionally manual so VBAT retention and
  5. real reset behavior are tested rather than simulated by a software command.
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import json
  10. import tempfile
  11. import time
  12. from pathlib import Path
  13. import serial
  14. from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
  15. CONTROL_BASE = 1200
  16. CONTROL_WINDOW_WORDS = 338
  17. CALL_REQUEST = CONTROL_BASE + 8
  18. CALL_RESPONSE = CONTROL_BASE + 24
  19. COMMAND_REQUEST = CONTROL_BASE + 40
  20. COMMAND_RESPONSE = CONTROL_BASE + 48
  21. AXIS_STATUS_BASE = CONTROL_BASE + 64
  22. AXIS_STATUS_WORDS = 48
  23. PERSISTENCE_BASE = CONTROL_BASE + 270
  24. PERSISTENCE_WORDS = 30
  25. PERSISTENCE_REQUEST = CONTROL_BASE + 300
  26. PERSISTENCE_RESPONSE = CONTROL_BASE + 308
  27. PERSISTENCE_VERSION = 1
  28. PERFORMANCE_VERSION = 7
  29. S0_BASES = (1600, 1800, 2000, 2200)
  30. S1_BASES = (1700, 1900, 2100, 2300)
  31. KNOWN_POSITIONS = (123456, -234567, 345678, -456789)
  32. RESULT_OK = 0
  33. RESULT_QUEUED = 1
  34. RESULT_BUSY = 8
  35. RESULT_NOT_SUPPORTED = 10
  36. PERSISTENCE_OK = 0
  37. PERSISTENCE_NOT_IMPLEMENTED = 4
  38. STATE_IDLE = 1
  39. STATE_ACCEL = 2
  40. STATE_RUN = 3
  41. STATE_DECEL = 4
  42. STATE_STOPPED = 8
  43. CALL_COMMIT = 1
  44. CALL_START = 2
  45. CMD_STOP_IMMEDIATE = 2
  46. CMD_SET_POSITION = 5
  47. CMD_SAVE_CONFIG = 8
  48. DESTRUCTIVE_MAGIC_A = 0xDA7A
  49. DESTRUCTIVE_MAGIC_B = 0x51F0
  50. DESTRUCTIVE_ARM = 0xA55A
  51. DESTRUCTIVE_CONFIRM = "INVALIDATE-NEWEST-SLOT"
  52. def put_u32(words: list[int], offset: int, value: int) -> None:
  53. words[offset : offset + 2] = signed_dword_words(value)
  54. def put_u64(words: list[int], offset: int, value: int) -> None:
  55. raw = value & 0xFFFFFFFFFFFFFFFF
  56. words[offset : offset + 4] = [
  57. (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48)
  58. ]
  59. def get_u32(words: list[int], offset: int) -> int:
  60. return words[offset] | (words[offset + 1] << 16)
  61. def get_u64(words: list[int], offset: int, signed: bool = False) -> int:
  62. raw = sum(words[offset + index] << (16 * index) for index in range(4))
  63. if signed and raw & (1 << 63):
  64. return raw - (1 << 64)
  65. return raw
  66. def next_sequence() -> int:
  67. value = int(time.time_ns() // 1_000_000) & 0x7FFFFFFF
  68. return value if value != 0 else 1
  69. def is_retryable_read_error(error: RuntimeError) -> bool:
  70. message = str(error)
  71. return message.startswith("响应超时:") or message.startswith("响应 CRC 错误:")
  72. def wait_response(
  73. client: RtuClient, address: int, count: int, sequence: int, timeout: float = 3.0
  74. ) -> list[int]:
  75. deadline = time.monotonic() + timeout
  76. latest: list[int] | None = None
  77. last_error: RuntimeError | None = None
  78. while time.monotonic() < deadline:
  79. try:
  80. latest = client.read_holding(address, count)
  81. except RuntimeError as error:
  82. if not is_retryable_read_error(error):
  83. raise
  84. last_error = error
  85. time.sleep(0.05)
  86. continue
  87. if get_u32(latest, 0) == sequence:
  88. return latest
  89. raise RuntimeError(
  90. f"等待序号 {sequence} 应答超时,最后应答={latest},"
  91. f"最后通信错误={last_error}"
  92. )
  93. def send_command(
  94. client: RtuClient,
  95. sequence: int,
  96. axis: int,
  97. opcode: int,
  98. argument: int = 0,
  99. response_timeout: float = 3.0,
  100. ) -> list[int]:
  101. request = [0] * 8
  102. put_u32(request, 0, sequence)
  103. request[2] = opcode
  104. request[3] = axis
  105. put_u64(request, 4, argument)
  106. client.write_multiple(COMMAND_REQUEST, request)
  107. return wait_response(
  108. client, COMMAND_RESPONSE, 8, sequence, timeout=response_timeout
  109. )
  110. def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]:
  111. words = client.read_holding(
  112. AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS, AXIS_STATUS_WORDS
  113. )
  114. generation_begin = get_u32(words, 0)
  115. generation_end = get_u32(words, 46)
  116. if generation_begin != generation_end or generation_begin & 1:
  117. raise RuntimeError(
  118. f"轴{axis}状态快照不一致:{generation_begin}/{generation_end}"
  119. )
  120. return {
  121. "state": words[2],
  122. "flags": get_u32(words, 3),
  123. "last_result": words[8],
  124. "last_sequence": get_u32(words, 10),
  125. "logical_position": get_u64(words, 16, signed=True),
  126. "task_pulses": get_u64(words, 20, signed=True),
  127. "current_frequency": get_u32(words, 38),
  128. }
  129. def wait_command_applied(
  130. client: RtuClient, axis: int, sequence: int, timeout: float = 5.0
  131. ) -> dict[str, int]:
  132. deadline = time.monotonic() + timeout
  133. latest: dict[str, int] | None = None
  134. last_error: RuntimeError | None = None
  135. while time.monotonic() < deadline:
  136. try:
  137. latest = read_axis_status(client, axis)
  138. except RuntimeError as error:
  139. if not is_retryable_read_error(error):
  140. raise
  141. last_error = error
  142. time.sleep(0.05)
  143. continue
  144. if latest["last_sequence"] == sequence:
  145. return latest
  146. raise RuntimeError(
  147. f"等待命令 {sequence} 内核执行超时,最后状态={latest},"
  148. f"最后通信错误={last_error}"
  149. )
  150. def read_persistence(client: RtuClient) -> dict[str, int]:
  151. latest: list[int] | None = None
  152. for _ in range(5):
  153. latest = client.read_holding(PERSISTENCE_BASE, PERSISTENCE_WORDS)
  154. generation_begin = get_u32(latest, 0)
  155. generation_end = get_u32(latest, 28)
  156. if generation_begin == generation_end and not generation_begin & 1:
  157. if latest[2] != PERSISTENCE_VERSION:
  158. raise RuntimeError(f"持久化诊断版本错误:{latest[2]}")
  159. hsd_newest = latest[4] & 0xFF
  160. sfd_newest = (latest[4] >> 8) & 0xFF
  161. hsd_generations = (get_u32(latest, 12), get_u32(latest, 14))
  162. sfd_generations = (get_u32(latest, 16), get_u32(latest, 18))
  163. return {
  164. "generation": generation_begin,
  165. "hsd_valid_mask": latest[3] & 0xFF,
  166. "sfd_valid_mask": (latest[3] >> 8) & 0xFF,
  167. "hsd_newest_mask": hsd_newest,
  168. "sfd_newest_mask": sfd_newest,
  169. "flags": latest[5],
  170. "last_hsd_load": latest[6],
  171. "last_sfd_load": latest[7],
  172. "last_hsd_save": latest[8],
  173. "last_sfd_save": latest[9],
  174. "last_sfd_erase": latest[10],
  175. "hsd_generation_a": hsd_generations[0],
  176. "hsd_generation_b": hsd_generations[1],
  177. "sfd_generation_a": sfd_generations[0],
  178. "sfd_generation_b": sfd_generations[1],
  179. "hsd_selected_generation": (
  180. hsd_generations[0]
  181. if hsd_newest == 1
  182. else hsd_generations[1] if hsd_newest == 2 else 0
  183. ),
  184. "sfd_selected_generation": (
  185. sfd_generations[0]
  186. if sfd_newest == 1
  187. else sfd_generations[1] if sfd_newest == 2 else 0
  188. ),
  189. "hsd_save_count": get_u32(latest, 20),
  190. "sfd_save_count": get_u32(latest, 22),
  191. "hsd_crc32": get_u32(latest, 24),
  192. "sfd_crc32": get_u32(latest, 26),
  193. }
  194. raise RuntimeError(f"持久化诊断快照连续不一致:{latest}")
  195. def wait_persistence_clean(client: RtuClient, timeout: float = 4.0) -> dict[str, int]:
  196. deadline = time.monotonic() + timeout
  197. latest: dict[str, int] | None = None
  198. while time.monotonic() < deadline:
  199. latest = read_persistence(client)
  200. if not latest["flags"] & 1 and latest["hsd_valid_mask"] != 0:
  201. return latest
  202. raise RuntimeError(f"HSD 检查点未在期限内完成:{latest}")
  203. def wait_hsd_checkpoint_advanced(
  204. client: RtuClient, baseline: dict[str, int], timeout: float = 4.0
  205. ) -> dict[str, int]:
  206. """Wait for the START safety checkpoint while motion keeps HSD dirty.
  207. Runtime position publication legitimately sets HSD dirty again on every
  208. pulse, so a running-axis test must verify a committed generation/save-count
  209. advance instead of waiting for the dirty flag to remain clear.
  210. """
  211. deadline = time.monotonic() + timeout
  212. latest: dict[str, int] | None = None
  213. while time.monotonic() < deadline:
  214. latest = read_persistence(client)
  215. if (
  216. latest["hsd_valid_mask"] != 0
  217. and latest["last_hsd_save"] == PERSISTENCE_OK
  218. and latest["hsd_selected_generation"]
  219. != baseline["hsd_selected_generation"]
  220. and latest["hsd_save_count"] != baseline["hsd_save_count"]
  221. ):
  222. return latest
  223. raise RuntimeError(f"START 的 HSD busy 检查点未在期限内提交:{latest}")
  224. def send_call(
  225. client: RtuClient, sequence: int, axis: int, operation: int
  226. ) -> list[int]:
  227. request = [0] * 16
  228. put_u32(request, 0, sequence)
  229. request[2] = 0
  230. put_u32(request, 3, S0_BASES[axis])
  231. request[5] = 0
  232. put_u32(request, 6, S1_BASES[axis])
  233. request[8] = 0
  234. put_u32(request, 10, 1)
  235. request[12] = axis
  236. request[13] = 0
  237. request[14] = operation
  238. client.write_multiple(CALL_REQUEST, request)
  239. return wait_response(client, CALL_RESPONSE, 12, sequence)
  240. def start_long_motion(client: RtuClient, sequence: int) -> int:
  241. s0 = [0] * 20
  242. put_u32(s0, 0, 1)
  243. put_u32(s0, 10, 100_000)
  244. put_u32(s0, 12, 50_000_000)
  245. client.write_multiple(S0_BASES[0], s0)
  246. client.write_multiple(S1_BASES[0], [0] * 4)
  247. response = send_call(client, sequence, 0, CALL_COMMIT)
  248. if response[3] != RESULT_OK or response[11] != 1:
  249. raise RuntimeError(f"长任务 COMMIT 失败:{response}")
  250. sequence += 1
  251. response = send_call(client, sequence, 0, CALL_START)
  252. if response[3] != RESULT_QUEUED:
  253. raise RuntimeError(f"长任务 START 失败:{response}")
  254. deadline = time.monotonic() + 5.0
  255. latest: dict[str, int] | None = None
  256. while time.monotonic() < deadline:
  257. latest = read_axis_status(client, 0)
  258. if (
  259. latest["last_sequence"] == sequence
  260. and latest["state"] in {STATE_ACCEL, STATE_RUN, STATE_DECEL}
  261. and latest["flags"] & (1 << 1)
  262. ):
  263. return sequence + 1
  264. raise RuntimeError(f"长任务未进入真实输出态:{latest}")
  265. def load_state(path: Path) -> dict[str, object]:
  266. if not path.exists():
  267. raise RuntimeError(f"阶段状态文件不存在:{path}")
  268. return json.loads(path.read_text(encoding="utf-8"))
  269. def save_state(path: Path, state: dict[str, object]) -> None:
  270. path.parent.mkdir(parents=True, exist_ok=True)
  271. path.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
  272. def print_diagnostics(diag: dict[str, int]) -> None:
  273. print(
  274. "持久化诊断:"
  275. f"HSD valid=0x{diag['hsd_valid_mask']:02X}, "
  276. f"gen={diag['hsd_selected_generation']}, crc=0x{diag['hsd_crc32']:08X}, "
  277. f"boot saves={diag['hsd_save_count']}; "
  278. f"SFD valid=0x{diag['sfd_valid_mask']:02X}, "
  279. f"gen={diag['sfd_selected_generation']}, crc=0x{diag['sfd_crc32']:08X}, "
  280. f"boot saves={diag['sfd_save_count']}; flags=0x{diag['flags']:04X}"
  281. )
  282. def phase_hsd_prepare(client: RtuClient, state_path: Path) -> None:
  283. sequence = next_sequence()
  284. for axis, position in enumerate(KNOWN_POSITIONS):
  285. response = send_command(client, sequence, axis, CMD_SET_POSITION, position)
  286. if response[4] != RESULT_QUEUED:
  287. raise RuntimeError(f"轴{axis} SET_POSITION 未排队:{response}")
  288. status = wait_command_applied(client, axis, sequence)
  289. if status["last_result"] != RESULT_OK or status["logical_position"] != position:
  290. raise RuntimeError(f"轴{axis}位置检查失败:{status}")
  291. sequence += 1
  292. diag = wait_persistence_clean(client)
  293. if diag["last_hsd_save"] != PERSISTENCE_OK:
  294. raise RuntimeError(f"HSD 保存结果不是 OK:{diag}")
  295. save_state(state_path, {"positions": list(KNOWN_POSITIONS)})
  296. print_diagnostics(diag)
  297. print("HSD prepare PASS。现在关闭主电源(保持 VBAT),再执行 --phase hsd-verify。")
  298. def phase_hsd_verify(client: RtuClient, state_path: Path) -> None:
  299. expected = [int(value) for value in load_state(state_path)["positions"]]
  300. for axis, position in enumerate(expected):
  301. status = read_axis_status(client, axis)
  302. if status["logical_position"] != position or not status["flags"] & (1 << 5):
  303. raise RuntimeError(f"轴{axis} HSD 恢复失败:期望{position},实测{status}")
  304. if status["flags"] & (1 << 1):
  305. raise RuntimeError(f"轴{axis} 上电后意外输出:{status}")
  306. diag = read_persistence(client)
  307. if diag["last_hsd_load"] != PERSISTENCE_OK:
  308. raise RuntimeError(f"HSD 上电加载结果不是 OK:{diag}")
  309. if not diag["flags"] & (1 << 2) or diag["flags"] & (1 << 3):
  310. raise RuntimeError(f"HSD restored flags 不符合正常停机:{diag}")
  311. print_diagnostics(diag)
  312. print("HSD VBAT 掉主电恢复 PASS。")
  313. def phase_busy_prepare(client: RtuClient) -> None:
  314. baseline = read_persistence(client)
  315. sequence = start_long_motion(client, next_sequence())
  316. diag = wait_hsd_checkpoint_advanced(client, baseline)
  317. print_diagnostics(diag)
  318. print(
  319. f"运行中掉电已就绪(下一序号{sequence})。现在直接关闭主电源或硬复位,"
  320. "不要先发 STOP;重启后执行 --phase busy-verify。"
  321. )
  322. def phase_busy_verify(client: RtuClient) -> None:
  323. for axis in range(4):
  324. status = read_axis_status(client, axis)
  325. if status["flags"] & (1 << 1):
  326. raise RuntimeError(f"轴{axis} 重启后仍有输出:{status}")
  327. if status["flags"] & (1 << 5):
  328. raise RuntimeError(f"轴{axis} 运行中复位后 position_valid 未清除:{status}")
  329. if status["state"] != STATE_IDLE:
  330. raise RuntimeError(f"轴{axis} 重启后不是 IDLE:{status}")
  331. diag = read_persistence(client)
  332. if not diag["flags"] & (1 << 3) or diag["flags"] & (1 << 2):
  333. raise RuntimeError(f"busy-reset restored flags 错误:{diag}")
  334. print_diagnostics(diag)
  335. print("运行中掉电/复位 PASS:未自动续跑,所有轴 position_valid=0。")
  336. def phase_sfd_save(client: RtuClient, state_path: Path) -> None:
  337. before = read_persistence(client)
  338. sequence = next_sequence()
  339. response = send_command(
  340. client, sequence, 0, CMD_SAVE_CONFIG, response_timeout=10.0
  341. )
  342. if response[4] != RESULT_QUEUED:
  343. raise RuntimeError(f"SAVE_CONFIG 未排队:{response}")
  344. status = wait_command_applied(client, 0, sequence, timeout=10.0)
  345. if status["last_result"] != RESULT_OK:
  346. raise RuntimeError(f"SAVE_CONFIG 内核执行失败:{status}")
  347. time.sleep(0.1)
  348. after = read_persistence(client)
  349. was_dirty = bool(before["flags"] & (1 << 1))
  350. if was_dirty:
  351. if after["sfd_save_count"] != before["sfd_save_count"] + 1:
  352. raise RuntimeError(f"SFD 实际保存次数不正确:before={before}, after={after}")
  353. if after["last_sfd_save"] != PERSISTENCE_OK:
  354. raise RuntimeError(f"SFD 保存结果不是 OK:{after}")
  355. if after["sfd_selected_generation"] == before["sfd_selected_generation"]:
  356. raise RuntimeError(f"SFD generation 未前进:before={before}, after={after}")
  357. elif after["sfd_save_count"] != before["sfd_save_count"]:
  358. raise RuntimeError("SFD clean no-op 不应擦写 Flash")
  359. if after["sfd_valid_mask"] == 0:
  360. raise RuntimeError(f"SFD 没有可验证的有效槽:{after}")
  361. state = load_state(state_path) if state_path.exists() else {}
  362. state["sfd_generation"] = after["sfd_selected_generation"]
  363. state["sfd_crc32"] = after["sfd_crc32"]
  364. save_state(state_path, state)
  365. print_diagnostics(after)
  366. if not was_dirty:
  367. print("SFD 当前为 clean,本次 SAVE 正确地没有重复擦写;验证现有提交记录。")
  368. print("SFD save PASS。现在硬复位/掉电重启,再执行 --phase sfd-verify。")
  369. def phase_sfd_verify(client: RtuClient, state_path: Path) -> None:
  370. state = load_state(state_path)
  371. expected_generation = int(state["sfd_generation"])
  372. expected_crc = int(state["sfd_crc32"])
  373. diag = read_persistence(client)
  374. if diag["last_sfd_load"] != PERSISTENCE_OK:
  375. raise RuntimeError(f"SFD 上电加载结果不是 OK:{diag}")
  376. if (
  377. diag["sfd_selected_generation"] != expected_generation
  378. or diag["sfd_crc32"] != expected_crc
  379. ):
  380. raise RuntimeError(
  381. "SFD 上电记录不一致:"
  382. f"期望 gen={expected_generation}, crc=0x{expected_crc:08X};实测={diag}"
  383. )
  384. print_diagnostics(diag)
  385. print("SFD A/B + CRC 上电加载 PASS。")
  386. def phase_motion_save_busy(client: RtuClient) -> None:
  387. sequence = start_long_motion(client, next_sequence())
  388. before = read_persistence(client)
  389. response = send_command(client, sequence, 0, CMD_SAVE_CONFIG)
  390. if response[4] != RESULT_QUEUED:
  391. raise RuntimeError(f"运行中 SAVE_CONFIG 未进入命令队列:{response}")
  392. status = wait_command_applied(client, 0, sequence)
  393. if status["last_result"] != RESULT_BUSY:
  394. raise RuntimeError(f"运行中 SAVE_CONFIG 未返回 BUSY:{status}")
  395. after = read_persistence(client)
  396. if after["sfd_save_count"] != before["sfd_save_count"]:
  397. raise RuntimeError("运行中 SAVE_CONFIG 意外擦写了 Flash")
  398. sequence += 1
  399. response = send_command(client, sequence, 0, CMD_STOP_IMMEDIATE)
  400. if response[4] != RESULT_QUEUED:
  401. raise RuntimeError(f"清理 STOP_IMMEDIATE 未排队:{response}")
  402. status = wait_command_applied(client, 0, sequence)
  403. if status["last_result"] != RESULT_OK or status["state"] != STATE_STOPPED:
  404. raise RuntimeError(f"清理停止失败:{status}")
  405. print_diagnostics(after)
  406. print("运行中禁止 SFD Flash 擦写 PASS:SAVE_CONFIG=BUSY,save_count 未增加。")
  407. def phase_invalidate(
  408. client: RtuClient, target: str, allow: bool, confirmation: str | None
  409. ) -> None:
  410. if not allow or confirmation != DESTRUCTIVE_CONFIRM:
  411. raise RuntimeError(
  412. "破坏性诊断未授权;必须同时使用 --allow-destructive "
  413. f"--confirm {DESTRUCTIVE_CONFIRM}"
  414. )
  415. before = read_persistence(client)
  416. if not before["flags"] & (1 << 4):
  417. raise RuntimeError("固件未显式启用 PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG")
  418. mask_key = "hsd_valid_mask" if target == "hsd" else "sfd_valid_mask"
  419. if before[mask_key] != 3:
  420. raise RuntimeError(f"必须先有两个有效槽,当前诊断={before}")
  421. sequence = next_sequence()
  422. inverse = (~sequence) & 0xFFFFFFFF
  423. request = [DESTRUCTIVE_MAGIC_A, DESTRUCTIVE_MAGIC_B]
  424. request += signed_dword_words(sequence)
  425. request += signed_dword_words(inverse)
  426. request += [1 if target == "hsd" else 2, DESTRUCTIVE_ARM]
  427. client.write_multiple(PERSISTENCE_REQUEST, request)
  428. response = wait_response(client, PERSISTENCE_RESPONSE, 8, sequence)
  429. if response[3] == RESULT_NOT_SUPPORTED:
  430. raise RuntimeError("破坏性诊断被固件拒绝(normal build)")
  431. if response[3] != RESULT_OK:
  432. raise RuntimeError(f"失效 newest {target.upper()} 槽失败:{response}")
  433. after = read_persistence(client)
  434. if after[mask_key] not in {1, 2}:
  435. raise RuntimeError(f"失效后应只剩一个有效槽:{after}")
  436. print_diagnostics(after)
  437. print(f"受控失效 newest {target.upper()} 槽 PASS;请立即执行对应 LOAD/重启回退验证。")
  438. def main() -> int:
  439. parser = argparse.ArgumentParser(description="PLSR HSD/SFD 真机掉电与 Flash 验收")
  440. parser.add_argument(
  441. "--phase",
  442. required=True,
  443. choices=(
  444. "diagnostics",
  445. "hsd-prepare",
  446. "hsd-verify",
  447. "busy-prepare",
  448. "busy-verify",
  449. "sfd-save",
  450. "sfd-verify",
  451. "motion-save-busy",
  452. "invalidate-hsd",
  453. "invalidate-sfd",
  454. ),
  455. )
  456. parser.add_argument("--port", default="COM5")
  457. parser.add_argument("--baud", type=int, default=9600)
  458. parser.add_argument("--slave", type=int, default=1)
  459. parser.add_argument(
  460. "--state-file",
  461. type=Path,
  462. default=Path(tempfile.gettempdir()) / "plsr_persistence_board_state.json",
  463. )
  464. parser.add_argument("--allow-destructive", action="store_true")
  465. parser.add_argument("--confirm")
  466. args = parser.parse_args()
  467. with serial.Serial(
  468. port=choose_port(args.port),
  469. baudrate=args.baud,
  470. bytesize=serial.EIGHTBITS,
  471. parity=serial.PARITY_EVEN,
  472. stopbits=serial.STOPBITS_ONE,
  473. timeout=1.0,
  474. write_timeout=1.0,
  475. ) as uart:
  476. client = RtuClient(uart, args.slave)
  477. header = client.read_holding(CONTROL_BASE, 8)
  478. if header[:5] != [0x504C, 0x5352, 0x0100, CONTROL_WINDOW_WORDS, 0x0007]:
  479. raise RuntimeError(f"控制窗口未就绪或固件过旧:{header}")
  480. if header[7] != PERFORMANCE_VERSION:
  481. raise RuntimeError(f"需要诊断版本 V{PERFORMANCE_VERSION},当前 V{header[7]}")
  482. print(f"控制窗口 D1200~D1537 就绪;阶段状态文件:{args.state_file}")
  483. if args.phase == "diagnostics":
  484. print_diagnostics(read_persistence(client))
  485. elif args.phase == "hsd-prepare":
  486. phase_hsd_prepare(client, args.state_file)
  487. elif args.phase == "hsd-verify":
  488. phase_hsd_verify(client, args.state_file)
  489. elif args.phase == "busy-prepare":
  490. phase_busy_prepare(client)
  491. elif args.phase == "busy-verify":
  492. phase_busy_verify(client)
  493. elif args.phase == "sfd-save":
  494. phase_sfd_save(client, args.state_file)
  495. elif args.phase == "sfd-verify":
  496. phase_sfd_verify(client, args.state_file)
  497. elif args.phase == "motion-save-busy":
  498. phase_motion_save_busy(client)
  499. elif args.phase == "invalidate-hsd":
  500. phase_invalidate(client, "hsd", args.allow_destructive, args.confirm)
  501. else:
  502. phase_invalidate(client, "sfd", args.allow_destructive, args.confirm)
  503. return 0
  504. if __name__ == "__main__":
  505. try:
  506. raise SystemExit(main())
  507. except (RuntimeError, serial.SerialException, KeyError, ValueError) as error:
  508. print(f"测试失败:{error}")
  509. raise SystemExit(1)