Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

1241 wiersze
48 KiB

  1. """Tkinter control panel for the 2026 PLSR Modbus register map."""
  2. from __future__ import annotations
  3. import argparse
  4. import queue
  5. import threading
  6. import time
  7. from datetime import datetime
  8. import tkinter as tk
  9. import tkinter.font as tkfont
  10. from tkinter import messagebox, ttk
  11. import serial
  12. from serial.tools import list_ports
  13. # Keep one RTU implementation for the board tests and this operator panel.
  14. from plsr_modbus_product_test import (
  15. COMMAND_CLEAR,
  16. COMMAND_START,
  17. COMMAND_STOP,
  18. COMMON_WORDS,
  19. CONFIG_BASE,
  20. CONTROL,
  21. DIAGNOSTIC_CLEAR,
  22. DIAGNOSTIC_CONTROL,
  23. DIAG_COUNT_CHECKED,
  24. DIAG_COUNT_PASS,
  25. DIAG_CURVE_CHECKED,
  26. DIAG_CURVE_PASS,
  27. DIAG_FAULT_LATCHED,
  28. DIAG_FREQUENCY_CHECKED,
  29. DIAG_FREQUENCY_PASS,
  30. DIAG_MONITORING,
  31. ModbusException,
  32. OUTPUT_AB,
  33. OUTPUT_MODE,
  34. OUTPUT_PULSE_DIR,
  35. RtuClient,
  36. SEGMENT_1_BASE,
  37. SEGMENT_WORDS,
  38. TestFailure,
  39. read_diagnostic,
  40. read_status,
  41. split_i32,
  42. split_u32,
  43. )
  44. BAUD_RATE = 9600
  45. SERIAL_TIMEOUT_SECONDS = 0.8
  46. DEFAULT_STATUS_POLL_MS = 250
  47. MIN_STATUS_POLL_MS = 1
  48. MAX_STATUS_POLL_MS = 3_600_000
  49. SEGMENT_COUNT = 10
  50. SEGMENT_STRIDE = 0x10
  51. MAX_FREQUENCY_HZ = 100_000
  52. OUTPUT_MODE_OPTIONS = (
  53. ("PULSE/DIR (0)", OUTPUT_PULSE_DIR),
  54. ("AB 正交脉冲 (1)", OUTPUT_AB),
  55. )
  56. PULSE_OPTIONS = (
  57. ("Y0 (0)", 0),
  58. ("Y1 (1)", 1),
  59. ("Y2 (2)", 2),
  60. ("Y3 (3)", 3),
  61. )
  62. DIRECTION_OPTIONS = (
  63. ("Y12 (0)", 0),
  64. ("Y13 (1)", 1),
  65. ("Y14 (2)", 2),
  66. ("Y15 (3)", 3),
  67. )
  68. INPUT_OPTIONS = (("X4 (0)", 0), ("X5 (1)", 1))
  69. SEND_OPTIONS = (("脉冲发送完毕 (0)", 0), ("后续段 (1)", 1))
  70. LOGIC_OPTIONS = (("正逻辑 (0)", 0), ("负逻辑 (1)", 1))
  71. CURVE_OPTIONS = (("直线 (0)", 0), ("S 曲线 (1)", 1), ("正弦曲线 (2)", 2))
  72. POSITION_OPTIONS = (("相对位置 (0)", 0), ("绝对位置 (1)", 1))
  73. WAIT_OPTIONS = (
  74. ("WAIT 时间 (0)", 0),
  75. ("WAIT 信号 (1)", 1),
  76. ("ACT 时间 (2)", 2),
  77. ("EXT 信号 (3)", 3),
  78. ("EXT 或完成 (4)", 4),
  79. )
  80. STATUS_NAMES = {
  81. 0: "未初始化",
  82. 1: "空闲",
  83. 2: "加速",
  84. 3: "运行",
  85. 4: "减速",
  86. 5: "等待",
  87. 6: "暂停",
  88. 7: "完成",
  89. 8: "停止",
  90. 9: "错误",
  91. }
  92. ERROR_NAMES = {
  93. 0: "无错误",
  94. 1: "状态转换非法",
  95. 2: "资源冲突",
  96. 3: "资源非法",
  97. 4: "定时器错误",
  98. 5: "计数错误",
  99. 6: "正限位",
  100. 7: "负限位",
  101. 8: "急停",
  102. 9: "内部错误",
  103. }
  104. DIAGNOSTIC_REASON_NAMES = {
  105. 0: "无故障",
  106. 1: "计数",
  107. 2: "频率",
  108. 3: "曲线",
  109. 4: "无效采样",
  110. }
  111. def option_labels(options):
  112. return tuple(label for label, _value in options)
  113. def option_label(options, value):
  114. for label, candidate in options:
  115. if candidate == value:
  116. return label
  117. return str(value)
  118. def option_value(options, label, field_name):
  119. for candidate_label, value in options:
  120. if candidate_label == label:
  121. return value
  122. raise ValueError("%s 不是有效选项" % field_name)
  123. def parse_integer(text, field_name, minimum, maximum):
  124. raw = text.strip()
  125. if not raw:
  126. raise ValueError("%s 不能为空" % field_name)
  127. try:
  128. value = int(raw, 0)
  129. except ValueError:
  130. try:
  131. value = int(raw, 10)
  132. except ValueError as error:
  133. raise ValueError("%s 必须是整数" % field_name) from error
  134. if value < minimum or value > maximum:
  135. raise ValueError(
  136. "%s 必须在 %d 到 %d 之间" % (field_name, minimum, maximum)
  137. )
  138. return value
  139. def write_common_configuration(client, words, output_mode):
  140. """Order writes so the intermediate configuration is always valid."""
  141. if output_mode == OUTPUT_PULSE_DIR:
  142. client.write_single(OUTPUT_MODE, OUTPUT_PULSE_DIR)
  143. client.write_multiple(CONFIG_BASE, words)
  144. if output_mode == OUTPUT_AB:
  145. client.write_single(OUTPUT_MODE, OUTPUT_AB)
  146. class SerialWorker(threading.Thread):
  147. """Own the serial port and report all results through a queue."""
  148. def __init__(self):
  149. super().__init__(name="plsr-serial", daemon=True)
  150. self.commands = queue.Queue()
  151. self.results = queue.Queue()
  152. self.stop_event = threading.Event()
  153. self.client = None
  154. self.next_status_poll = 0.0
  155. self.polling_enabled = True
  156. self.poll_interval_seconds = DEFAULT_STATUS_POLL_MS / 1000.0
  157. self.log_callback = None
  158. def submit(self, command, **payload):
  159. self.commands.put((command, payload))
  160. def configure_polling(self, enabled, interval_ms):
  161. self.polling_enabled = bool(enabled)
  162. self.poll_interval_seconds = interval_ms / 1000.0
  163. self.next_status_poll = time.monotonic() + self.poll_interval_seconds
  164. def set_log_callback(self, callback):
  165. self.log_callback = callback
  166. def _log(self, message):
  167. if self.log_callback is not None:
  168. self.log_callback("%s %s" % (datetime.now().strftime("%H:%M:%S.%f")[:-3], message))
  169. def close(self):
  170. self.stop_event.set()
  171. self.commands.put(("shutdown", {}))
  172. def _emit(self, event, **payload):
  173. self.results.put((event, payload))
  174. def _disconnect(self, notify=True, reason=""):
  175. client = self.client
  176. self.client = None
  177. if client is not None:
  178. try:
  179. client.__exit__(None, None, None)
  180. except (OSError, serial.SerialException):
  181. pass
  182. if notify:
  183. self._emit("connection", connected=False, reason=reason)
  184. def _require_client(self):
  185. if self.client is None:
  186. raise RuntimeError("串口尚未连接")
  187. return self.client
  188. def _read_configuration(self):
  189. client = self._require_client()
  190. common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
  191. output_mode = client.read_holding(OUTPUT_MODE, 1)[0]
  192. segments = []
  193. for index in range(SEGMENT_COUNT):
  194. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  195. segments.append(client.read_holding(address, SEGMENT_WORDS))
  196. self._emit(
  197. "configuration",
  198. common=common,
  199. output_mode=output_mode,
  200. segments=segments,
  201. )
  202. def _handle_connection_error(self, operation, error):
  203. self._disconnect(notify=True, reason=str(error))
  204. self._emit("error", operation=operation, message=str(error), modal=True)
  205. def _handle_command(self, command, payload):
  206. if command == "connect":
  207. self._disconnect(notify=False)
  208. try:
  209. client = RtuClient(
  210. payload["port"],
  211. BAUD_RATE,
  212. payload["slave"],
  213. SERIAL_TIMEOUT_SECONDS,
  214. )
  215. client.__enter__()
  216. client.log_callback = self._log
  217. self.client = client
  218. self.next_status_poll = 0.0
  219. self._emit(
  220. "connection",
  221. connected=True,
  222. port=payload["port"],
  223. slave=payload["slave"],
  224. )
  225. self._read_configuration()
  226. except (OSError, serial.SerialException) as error:
  227. self._handle_connection_error("连接", error)
  228. except (TestFailure, ModbusException, RuntimeError) as error:
  229. self._disconnect(notify=True, reason=str(error))
  230. self._emit(
  231. "error", operation="读取参数", message=str(error), modal=True
  232. )
  233. return
  234. if command == "disconnect":
  235. self._disconnect(notify=True)
  236. return
  237. try:
  238. client = self._require_client()
  239. if command == "read_configuration":
  240. self._read_configuration()
  241. self._emit("operation", message="参数读取完成")
  242. elif command == "write_common":
  243. write_common_configuration(
  244. client, payload["words"], payload["output_mode"]
  245. )
  246. common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
  247. output_mode = client.read_holding(OUTPUT_MODE, 1)[0]
  248. self._emit("common", words=common, output_mode=output_mode)
  249. self._emit("operation", message="公共参数写入并回读完成")
  250. elif command == "write_segments":
  251. for index, words in enumerate(payload["segments"]):
  252. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  253. client.write_multiple(address, words)
  254. segments = []
  255. for index in range(SEGMENT_COUNT):
  256. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  257. segments.append(client.read_holding(address, SEGMENT_WORDS))
  258. self._emit("segments", words=segments)
  259. self._emit("operation", message="10 段参数写入并回读完成")
  260. elif command == "control":
  261. client.write_single(CONTROL, payload["value"])
  262. self._emit("operation", message=payload["message"])
  263. elif command == "clear_diagnostic":
  264. client.write_single(DIAGNOSTIC_CONTROL, DIAGNOSTIC_CLEAR)
  265. self._emit("diagnostic", diagnostic=read_diagnostic(client))
  266. self._emit("operation", message="诊断锁存和统计已清除")
  267. else:
  268. raise RuntimeError("未知后台命令: %s" % command)
  269. except (OSError, serial.SerialException) as error:
  270. self._handle_connection_error(payload.get("operation", "通信"), error)
  271. except (TestFailure, ModbusException, RuntimeError) as error:
  272. self._emit(
  273. "error",
  274. operation=payload.get("operation", "通信"),
  275. message=str(error),
  276. modal=True,
  277. )
  278. def _poll_status(self):
  279. try:
  280. status = read_status(self._require_client())
  281. diagnostic = read_diagnostic(self._require_client())
  282. self._emit("status", status=status)
  283. self._emit("diagnostic", diagnostic=diagnostic)
  284. self.next_status_poll = time.monotonic() + self.poll_interval_seconds
  285. except (OSError, serial.SerialException) as error:
  286. self._handle_connection_error("状态轮询", error)
  287. except (TestFailure, ModbusException, RuntimeError) as error:
  288. self._emit(
  289. "error", operation="状态轮询", message=str(error), modal=False
  290. )
  291. self.next_status_poll = time.monotonic() + self.poll_interval_seconds
  292. def run(self):
  293. try:
  294. while not self.stop_event.is_set():
  295. try:
  296. command, payload = self.commands.get(timeout=0.05)
  297. except queue.Empty:
  298. command = None
  299. payload = None
  300. if command == "shutdown":
  301. break
  302. if command is not None:
  303. self._handle_command(command, payload)
  304. if (
  305. self.polling_enabled
  306. and self.client is not None
  307. and time.monotonic() >= self.next_status_poll
  308. ):
  309. self._poll_status()
  310. finally:
  311. self._disconnect(notify=False)
  312. class PlsrControlPanel:
  313. def __init__(self, root):
  314. self.root = root
  315. self.root.title("PLSR 控制面板")
  316. self.root.geometry("1180x720")
  317. self.root.minsize(980, 650)
  318. self.root.protocol("WM_DELETE_WINDOW", self._on_close)
  319. self.connected = False
  320. self.operation_pending = False
  321. self.worker = SerialWorker()
  322. self.worker.set_log_callback(self._queue_log)
  323. self.worker.start()
  324. self.log_window = None
  325. self.log_text = None
  326. self.port_var = tk.StringVar()
  327. self.slave_var = tk.StringVar(value="1")
  328. self.polling_enabled_var = tk.BooleanVar(value=True)
  329. self.poll_interval_var = tk.StringVar(value=str(DEFAULT_STATUS_POLL_MS))
  330. self.connection_var = tk.StringVar(value="未连接")
  331. self.footer_var = tk.StringVar(value="请选择串口并连接")
  332. self.status_vars = {
  333. "position": tk.StringVar(value="--"),
  334. "frequency": tk.StringVar(value="--"),
  335. "state": tk.StringVar(value="--"),
  336. "segment": tk.StringVar(value="--"),
  337. "error": tk.StringVar(value="--"),
  338. }
  339. self.diagnostic_vars = {
  340. key: tk.StringVar(value="--")
  341. for key in (
  342. "summary", "reason", "segment", "mode_direction",
  343. "expected_count", "actual_count", "count_error",
  344. "requested_hz", "expected_hz", "active_hz",
  345. "request_error", "sample_count", "mismatch_count",
  346. "maximum_error", "first_mismatch",
  347. )
  348. }
  349. self.common_vars = {}
  350. self.segment_vars = []
  351. self.connection_widgets = []
  352. self.action_buttons = []
  353. self._configure_style()
  354. self._build_ui()
  355. self.refresh_ports()
  356. self._set_connected(False)
  357. self.root.after(50, self._drain_results)
  358. def _configure_style(self):
  359. default_font = tkfont.nametofont("TkDefaultFont")
  360. default_font.configure(family="Microsoft YaHei UI", size=10)
  361. text_font = tkfont.nametofont("TkTextFont")
  362. text_font.configure(family="Microsoft YaHei UI", size=10)
  363. style = ttk.Style(self.root)
  364. style.configure("Connected.TLabel", foreground="#18794e")
  365. style.configure("Disconnected.TLabel", foreground="#5f6368")
  366. style.configure("Error.TLabel", foreground="#b42318")
  367. style.configure("StatusValue.TLabel", font=("Microsoft YaHei UI", 11, "bold"))
  368. def _build_ui(self):
  369. self.root.columnconfigure(0, weight=1)
  370. self.root.rowconfigure(2, weight=1)
  371. connection = ttk.LabelFrame(self.root, text="连接")
  372. connection.grid(row=0, column=0, padx=10, pady=(10, 6), sticky="ew")
  373. connection.columnconfigure(7, weight=1)
  374. ttk.Label(connection, text="串口").grid(row=0, column=0, padx=(8, 4), pady=8)
  375. self.port_box = ttk.Combobox(
  376. connection, textvariable=self.port_var, width=13, state="readonly"
  377. )
  378. self.port_box.grid(row=0, column=1, padx=4, pady=8)
  379. refresh_button = ttk.Button(connection, text="刷新", command=self.refresh_ports)
  380. refresh_button.grid(row=0, column=2, padx=(0, 12), pady=8)
  381. ttk.Label(connection, text="串口参数").grid(row=0, column=3, padx=4, pady=8)
  382. ttk.Label(connection, text="9600, 8E1").grid(row=0, column=4, padx=(4, 12), pady=8)
  383. ttk.Label(connection, text="从站地址").grid(row=0, column=5, padx=4, pady=8)
  384. slave_entry = ttk.Entry(connection, textvariable=self.slave_var, width=7)
  385. slave_entry.grid(row=0, column=6, padx=(4, 12), pady=8)
  386. self.connect_button = ttk.Button(
  387. connection, text="连接", command=self._toggle_connection
  388. )
  389. self.connect_button.grid(row=0, column=8, padx=8, pady=8)
  390. polling = ttk.LabelFrame(connection, text="状态轮询")
  391. polling.grid(row=1, column=0, columnspan=9, padx=8, pady=(0, 6), sticky="ew")
  392. ttk.Checkbutton(
  393. polling, text="启动轮询", variable=self.polling_enabled_var,
  394. command=self._apply_polling_settings,
  395. ).grid(row=0, column=0, padx=(8, 4), pady=6)
  396. ttk.Label(polling, text="轮询间隔").grid(row=0, column=1, padx=(12, 4), pady=6)
  397. ttk.Entry(polling, textvariable=self.poll_interval_var, width=9).grid(
  398. row=0, column=2, padx=4, pady=6
  399. )
  400. ttk.Label(polling, text="ms(最小 1 ms)").grid(
  401. row=0, column=3, padx=(4, 8), pady=6
  402. )
  403. self.connection_label = ttk.Label(
  404. connection,
  405. textvariable=self.connection_var,
  406. style="Disconnected.TLabel",
  407. width=28,
  408. anchor="e",
  409. )
  410. self.connection_label.grid(row=0, column=7, padx=8, pady=8, sticky="e")
  411. self.connection_widgets = [self.port_box, refresh_button, slave_entry]
  412. ttk.Button(connection, text="通信日志", command=self._show_log_window).grid(
  413. row=0, column=9, padx=8, pady=8
  414. )
  415. self._build_runtime_status()
  416. notebook = ttk.Notebook(self.root)
  417. notebook.grid(row=2, column=0, padx=10, pady=6, sticky="nsew")
  418. common_tab = ttk.Frame(notebook, padding=12)
  419. segment_tab = ttk.Frame(notebook, padding=12)
  420. diagnostic_tab = ttk.Frame(notebook, padding=12)
  421. notebook.add(common_tab, text="公共参数 0x1000-0x1013")
  422. notebook.add(segment_tab, text="10 段参数 0x1100-0x1197")
  423. notebook.add(diagnostic_tab, text="输出诊断 0x2100-0x2119")
  424. self._build_common_tab(common_tab)
  425. self._build_segment_tab(segment_tab)
  426. self._build_diagnostic_tab(diagnostic_tab)
  427. footer = ttk.Frame(self.root)
  428. footer.grid(row=3, column=0, padx=10, pady=(4, 10), sticky="ew")
  429. footer.columnconfigure(4, weight=1)
  430. read_button = ttk.Button(footer, text="读取全部参数", command=self._read_all)
  431. read_button.grid(row=0, column=0, padx=(0, 6))
  432. common_button = ttk.Button(
  433. footer, text="写入公共参数", command=self._write_common
  434. )
  435. common_button.grid(row=0, column=1, padx=6)
  436. segment_button = ttk.Button(
  437. footer, text="写入 10 段参数", command=self._write_segments
  438. )
  439. segment_button.grid(row=0, column=2, padx=6)
  440. self.action_buttons.extend([read_button, common_button, segment_button])
  441. self.footer_label = ttk.Label(
  442. footer, textvariable=self.footer_var, anchor="e"
  443. )
  444. self.footer_label.grid(row=0, column=4, padx=(12, 0), sticky="ew")
  445. def _build_runtime_status(self):
  446. frame = ttk.LabelFrame(self.root, text="实时状态 0x2000-0x2006")
  447. frame.grid(row=1, column=0, padx=10, pady=6, sticky="ew")
  448. for column in range(11):
  449. frame.columnconfigure(column, weight=1 if column % 2 else 0)
  450. fields = (
  451. ("累计位置", "position"),
  452. ("当前频率", "frequency"),
  453. ("运行状态", "state"),
  454. ("当前段", "segment"),
  455. ("错误码", "error"),
  456. )
  457. for index, (label, key) in enumerate(fields):
  458. ttk.Label(frame, text=label).grid(
  459. row=0, column=index * 2, padx=(8, 4), pady=9, sticky="e"
  460. )
  461. ttk.Label(
  462. frame, textvariable=self.status_vars[key], style="StatusValue.TLabel"
  463. ).grid(row=0, column=index * 2 + 1, padx=(4, 12), pady=9, sticky="w")
  464. commands = ttk.Frame(frame)
  465. commands.grid(row=0, column=10, padx=8, pady=6, sticky="e")
  466. start_button = ttk.Button(
  467. commands, text="启动", command=lambda: self._send_control(COMMAND_START)
  468. )
  469. stop_button = ttk.Button(
  470. commands, text="停止", command=lambda: self._send_control(COMMAND_STOP)
  471. )
  472. clear_button = ttk.Button(commands, text="清零", command=self._clear_position)
  473. start_button.grid(row=0, column=0, padx=3)
  474. stop_button.grid(row=0, column=1, padx=3)
  475. clear_button.grid(row=0, column=2, padx=3)
  476. self.action_buttons.extend([start_button, stop_button, clear_button])
  477. def _add_entry(self, parent, row, group, key, label, default="0", width=13):
  478. column = group * 2
  479. ttk.Label(parent, text=label).grid(
  480. row=row, column=column, padx=(8, 4), pady=7, sticky="e"
  481. )
  482. variable = tk.StringVar(value=default)
  483. ttk.Entry(parent, textvariable=variable, width=width).grid(
  484. row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w"
  485. )
  486. self.common_vars[key] = variable
  487. def _add_combo(self, parent, row, group, key, label, options):
  488. column = group * 2
  489. ttk.Label(parent, text=label).grid(
  490. row=row, column=column, padx=(8, 4), pady=7, sticky="e"
  491. )
  492. variable = tk.StringVar(value=options[0][0])
  493. ttk.Combobox(
  494. parent,
  495. textvariable=variable,
  496. values=option_labels(options),
  497. state="readonly",
  498. width=18,
  499. ).grid(row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w")
  500. self.common_vars[key] = variable
  501. def _build_common_tab(self, parent):
  502. for column in (1, 3, 5):
  503. parent.columnconfigure(column, weight=1)
  504. self._add_combo(
  505. parent, 0, 0, "pulse_output", "脉冲输出(单逻辑 PLSR)", PULSE_OPTIONS
  506. )
  507. self._add_combo(
  508. parent, 0, 1, "direction_output", "方向输出", DIRECTION_OPTIONS
  509. )
  510. self._add_entry(parent, 0, 2, "direction_delay", "方向延时 ms")
  511. self._add_combo(parent, 1, 0, "wait_input", "WAIT 输入", INPUT_OPTIONS)
  512. self._add_combo(parent, 1, 1, "ext_input", "EXT 输入", INPUT_OPTIONS)
  513. self._add_combo(parent, 1, 2, "send_mode", "发送模式", SEND_OPTIONS)
  514. self._add_combo(parent, 2, 0, "negative_logic", "方向逻辑", LOGIC_OPTIONS)
  515. self._add_combo(parent, 2, 1, "curve_mode", "曲线模式", CURVE_OPTIONS)
  516. self._add_combo(parent, 2, 2, "position_mode", "位置模式", POSITION_OPTIONS)
  517. self._add_entry(parent, 3, 0, "segment_count", "段数", default="1")
  518. self._add_entry(parent, 3, 1, "start_segment", "起始段", default="1")
  519. self._add_entry(parent, 3, 2, "default_speed", "默认速度 Hz", default="1000")
  520. self._add_entry(parent, 4, 0, "start_speed", "启动速度 Hz", default="100")
  521. self._add_entry(parent, 4, 1, "stop_speed", "停止速度 Hz", default="100")
  522. self._add_entry(parent, 4, 2, "acceleration", "加速时间 ms")
  523. self._add_entry(parent, 5, 0, "deceleration", "减速时间 ms")
  524. self._add_combo(
  525. parent, 5, 1, "output_mode", "输出模式 0x1200", OUTPUT_MODE_OPTIONS
  526. )
  527. def _build_diagnostic_tab(self, parent):
  528. parent.columnconfigure(1, weight=1)
  529. parent.columnconfigure(3, weight=1)
  530. parent.columnconfigure(5, weight=1)
  531. fields = (
  532. ("综合结果", "summary"),
  533. ("首个故障", "reason"),
  534. ("段号", "segment"),
  535. ("模式 / 方向", "mode_direction"),
  536. ("期望脉冲", "expected_count"),
  537. ("实测脉冲", "actual_count"),
  538. ("计数误差", "count_error"),
  539. ("请求频率", "requested_hz"),
  540. ("量化频率", "expected_hz"),
  541. ("活动频率", "active_hz"),
  542. ("请求量化误差", "request_error"),
  543. ("曲线采样数", "sample_count"),
  544. ("不匹配数", "mismatch_count"),
  545. ("最大频率误差", "maximum_error"),
  546. ("首个不匹配样本", "first_mismatch"),
  547. )
  548. for index, (label, key) in enumerate(fields):
  549. row = index // 3
  550. group = index % 3
  551. column = group * 2
  552. ttk.Label(parent, text=label).grid(
  553. row=row, column=column, padx=(8, 4), pady=9, sticky="e"
  554. )
  555. ttk.Label(
  556. parent,
  557. textvariable=self.diagnostic_vars[key],
  558. style="StatusValue.TLabel" if key == "summary" else "TLabel",
  559. ).grid(row=row, column=column + 1, padx=(4, 18), pady=9, sticky="w")
  560. clear_button = ttk.Button(
  561. parent, text="清除诊断", command=self._clear_diagnostic
  562. )
  563. clear_button.grid(row=5, column=5, padx=8, pady=(16, 4), sticky="e")
  564. self.action_buttons.append(clear_button)
  565. def _build_segment_tab(self, parent):
  566. headers = (
  567. ("段 / 基址", 15),
  568. ("频率 Hz", 13),
  569. ("脉冲数", 15),
  570. ("等待类型", 19),
  571. ("WAIT ms", 11),
  572. ("ACT ms", 11),
  573. ("跳转段", 9),
  574. )
  575. for column, (label, _width) in enumerate(headers):
  576. parent.columnconfigure(column, weight=1 if column in (1, 2, 3) else 0)
  577. ttk.Label(parent, text=label, anchor="center").grid(
  578. row=0, column=column, padx=4, pady=(0, 6), sticky="ew"
  579. )
  580. for index in range(SEGMENT_COUNT):
  581. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  582. ttk.Label(parent, text="%d / 0x%04X" % (index + 1, address)).grid(
  583. row=index + 1, column=0, padx=(2, 8), pady=4, sticky="e"
  584. )
  585. variables = {
  586. "frequency": tk.StringVar(value="1000"),
  587. "pulses": tk.StringVar(value="0"),
  588. "wait_type": tk.StringVar(value=WAIT_OPTIONS[0][0]),
  589. "wait_ms": tk.StringVar(value="0"),
  590. "act_ms": tk.StringVar(value="0"),
  591. "jump": tk.StringVar(value="0"),
  592. }
  593. ttk.Entry(parent, textvariable=variables["frequency"], width=13).grid(
  594. row=index + 1, column=1, padx=4, pady=4, sticky="ew"
  595. )
  596. ttk.Entry(parent, textvariable=variables["pulses"], width=15).grid(
  597. row=index + 1, column=2, padx=4, pady=4, sticky="ew"
  598. )
  599. ttk.Combobox(
  600. parent,
  601. textvariable=variables["wait_type"],
  602. values=option_labels(WAIT_OPTIONS),
  603. state="readonly",
  604. width=19,
  605. ).grid(row=index + 1, column=3, padx=4, pady=4, sticky="ew")
  606. ttk.Entry(parent, textvariable=variables["wait_ms"], width=11).grid(
  607. row=index + 1, column=4, padx=4, pady=4
  608. )
  609. ttk.Entry(parent, textvariable=variables["act_ms"], width=11).grid(
  610. row=index + 1, column=5, padx=4, pady=4
  611. )
  612. ttk.Entry(parent, textvariable=variables["jump"], width=9).grid(
  613. row=index + 1, column=6, padx=4, pady=4
  614. )
  615. self.segment_vars.append(variables)
  616. def refresh_ports(self):
  617. ports = [item.device for item in list_ports.comports()]
  618. self.port_box["values"] = ports
  619. if ports and self.port_var.get() not in ports:
  620. self.port_var.set(ports[0])
  621. elif not ports:
  622. self.port_var.set("")
  623. def _queue_log(self, message):
  624. self.worker.results.put(("log", {"message": message}))
  625. def _show_log_window(self):
  626. if self.log_window is not None and self.log_window.winfo_exists():
  627. self.log_window.deiconify()
  628. self.log_window.lift()
  629. return
  630. self.log_window = tk.Toplevel(self.root)
  631. self.log_window.title("Modbus 通信日志")
  632. self.log_window.geometry("760x420")
  633. self.log_text = tk.Text(self.log_window, wrap="none", state="disabled")
  634. self.log_text.pack(fill="both", expand=True, padx=8, pady=8)
  635. ttk.Button(self.log_window, text="清空日志", command=self._clear_log).pack(pady=(0, 8))
  636. def _clear_log(self):
  637. if self.log_text is not None:
  638. self.log_text.configure(state="normal")
  639. self.log_text.delete("1.0", "end")
  640. self.log_text.configure(state="disabled")
  641. def _polling_settings(self):
  642. interval_ms = parse_integer(
  643. self.poll_interval_var.get(), "轮询间隔", MIN_STATUS_POLL_MS, MAX_STATUS_POLL_MS
  644. )
  645. return self.polling_enabled_var.get(), interval_ms
  646. def _apply_polling_settings(self):
  647. try:
  648. enabled, interval_ms = self._polling_settings()
  649. except ValueError as error:
  650. messagebox.showerror("轮询设置", str(error))
  651. return
  652. self.worker.configure_polling(enabled, interval_ms)
  653. self.footer_var.set(
  654. "轮询已启动,间隔 %d ms" % interval_ms if enabled else "轮询已停止"
  655. )
  656. def _set_connected(self, connected, description=""):
  657. self.connected = connected
  658. self.connect_button.configure(
  659. text="断开" if connected else "连接", state="normal"
  660. )
  661. connection_state = "disabled" if connected else "normal"
  662. for widget in self.connection_widgets:
  663. if widget is self.port_box:
  664. widget.configure(state="disabled" if connected else "readonly")
  665. else:
  666. widget.configure(state=connection_state)
  667. if connected:
  668. self.connection_var.set(description or "已连接")
  669. self.connection_label.configure(style="Connected.TLabel")
  670. else:
  671. self.connection_var.set(description or "未连接")
  672. self.connection_label.configure(
  673. style="Error.TLabel" if description else "Disconnected.TLabel"
  674. )
  675. for variable in self.status_vars.values():
  676. variable.set("--")
  677. for variable in self.diagnostic_vars.values():
  678. variable.set("--")
  679. self._update_action_state()
  680. def _update_action_state(self):
  681. state = "normal" if self.connected and not self.operation_pending else "disabled"
  682. for button in self.action_buttons:
  683. button.configure(state=state)
  684. def _start_operation(self, message):
  685. if not self.connected:
  686. messagebox.showerror("未连接", "请先连接 PLSR 从站")
  687. return False
  688. if self.operation_pending:
  689. return False
  690. self.operation_pending = True
  691. self.footer_var.set(message)
  692. self._update_action_state()
  693. return True
  694. def _finish_operation(self, message):
  695. self.operation_pending = False
  696. self.footer_var.set(message)
  697. self.footer_label.configure(style="TLabel")
  698. self._update_action_state()
  699. def _toggle_connection(self):
  700. if self.connected:
  701. self.operation_pending = False
  702. self.footer_var.set("正在断开")
  703. self.worker.submit("disconnect")
  704. return
  705. port = self.port_var.get().strip()
  706. if not port:
  707. messagebox.showerror("连接参数", "请选择可用串口")
  708. return
  709. try:
  710. slave = parse_integer(self.slave_var.get(), "从站地址", 1, 247)
  711. except ValueError as error:
  712. messagebox.showerror("连接参数", str(error))
  713. return
  714. self.connect_button.configure(state="disabled")
  715. self.operation_pending = True
  716. self._update_action_state()
  717. self.connection_var.set("正在连接 %s" % port)
  718. self.footer_var.set("正在打开串口并读取参数")
  719. try:
  720. enabled, interval_ms = self._polling_settings()
  721. except ValueError as error:
  722. self.connect_button.configure(state="normal")
  723. self.operation_pending = False
  724. self._update_action_state()
  725. messagebox.showerror("轮询设置", str(error))
  726. return
  727. self.worker.configure_polling(enabled, interval_ms)
  728. self.worker.submit("connect", port=port, slave=slave)
  729. def _common_words(self):
  730. values = self.common_vars
  731. segment_count = parse_integer(values["segment_count"].get(), "段数", 1, 10)
  732. start_segment = parse_integer(
  733. values["start_segment"].get(), "起始段", 1, 10
  734. )
  735. if start_segment > segment_count:
  736. raise ValueError("起始段不能大于段数")
  737. words = [0] * COMMON_WORDS
  738. words[0] = option_value(
  739. PULSE_OPTIONS, values["pulse_output"].get(), "脉冲输出"
  740. )
  741. words[1] = option_value(
  742. DIRECTION_OPTIONS, values["direction_output"].get(), "方向输出"
  743. )
  744. words[2] = option_value(INPUT_OPTIONS, values["wait_input"].get(), "WAIT 输入")
  745. words[3] = option_value(INPUT_OPTIONS, values["ext_input"].get(), "EXT 输入")
  746. words[4] = option_value(SEND_OPTIONS, values["send_mode"].get(), "发送模式")
  747. words[5] = parse_integer(
  748. values["direction_delay"].get(), "方向延时", 0, 0xFFFF
  749. )
  750. words[6] = option_value(
  751. LOGIC_OPTIONS, values["negative_logic"].get(), "方向逻辑"
  752. )
  753. words[7] = option_value(CURVE_OPTIONS, values["curve_mode"].get(), "曲线模式")
  754. words[8] = option_value(
  755. POSITION_OPTIONS, values["position_mode"].get(), "位置模式"
  756. )
  757. words[9] = segment_count
  758. words[10] = start_segment
  759. words[11:13] = split_u32(
  760. parse_integer(
  761. values["default_speed"].get(), "默认速度", 1, MAX_FREQUENCY_HZ
  762. )
  763. )
  764. words[13:15] = split_u32(
  765. parse_integer(
  766. values["start_speed"].get(), "启动速度", 0, MAX_FREQUENCY_HZ
  767. )
  768. )
  769. words[15] = 0
  770. words[16:18] = split_u32(
  771. parse_integer(
  772. values["stop_speed"].get(), "停止速度", 0, MAX_FREQUENCY_HZ
  773. )
  774. )
  775. words[18] = parse_integer(values["acceleration"].get(), "加速时间", 0, 0xFFFF)
  776. words[19] = parse_integer(values["deceleration"].get(), "减速时间", 0, 0xFFFF)
  777. return words
  778. def _segment_words(self):
  779. segment_count = parse_integer(
  780. self.common_vars["segment_count"].get(), "段数", 1, SEGMENT_COUNT
  781. )
  782. encoded = []
  783. for index, variables in enumerate(self.segment_vars):
  784. name = "第 %d 段" % (index + 1)
  785. frequency = parse_integer(
  786. variables["frequency"].get(), name + "频率", 0, MAX_FREQUENCY_HZ
  787. )
  788. pulses = parse_integer(
  789. variables["pulses"].get(), name + "脉冲数", -(1 << 31), (1 << 31) - 1
  790. )
  791. wait_type = option_value(
  792. WAIT_OPTIONS, variables["wait_type"].get(), name + "等待类型"
  793. )
  794. wait_ms = parse_integer(
  795. variables["wait_ms"].get(), name + " WAIT 时间", 0, 0xFFFF
  796. )
  797. act_ms = parse_integer(
  798. variables["act_ms"].get(), name + " ACT 时间", 0, 0xFFFF
  799. )
  800. jump = parse_integer(variables["jump"].get(), name + "跳转段", 0, 10)
  801. if index < segment_count and jump > segment_count:
  802. raise ValueError(
  803. "%s跳转段必须为 0 或不大于脉冲总段数 %d"
  804. % (name, segment_count)
  805. )
  806. words = split_u32(frequency) + split_i32(pulses)
  807. words.extend((wait_type, wait_ms, act_ms, jump))
  808. encoded.append(words)
  809. return encoded
  810. def _set_common_words(self, words):
  811. if len(words) != COMMON_WORDS:
  812. raise ValueError("公共参数回读长度错误")
  813. values = self.common_vars
  814. values["pulse_output"].set(option_label(PULSE_OPTIONS, words[0]))
  815. values["direction_output"].set(option_label(DIRECTION_OPTIONS, words[1]))
  816. values["wait_input"].set(option_label(INPUT_OPTIONS, words[2]))
  817. values["ext_input"].set(option_label(INPUT_OPTIONS, words[3]))
  818. values["send_mode"].set(option_label(SEND_OPTIONS, words[4]))
  819. values["direction_delay"].set(str(words[5]))
  820. values["negative_logic"].set(option_label(LOGIC_OPTIONS, words[6]))
  821. values["curve_mode"].set(option_label(CURVE_OPTIONS, words[7]))
  822. values["position_mode"].set(option_label(POSITION_OPTIONS, words[8]))
  823. values["segment_count"].set(str(words[9]))
  824. values["start_segment"].set(str(words[10]))
  825. values["default_speed"].set(str(words[11] | (words[12] << 16)))
  826. values["start_speed"].set(str(words[13] | (words[14] << 16)))
  827. values["stop_speed"].set(str(words[16] | (words[17] << 16)))
  828. values["acceleration"].set(str(words[18]))
  829. values["deceleration"].set(str(words[19]))
  830. def _set_segment_words(self, segments):
  831. if len(segments) != SEGMENT_COUNT:
  832. raise ValueError("段参数回读数量错误")
  833. for index, words in enumerate(segments):
  834. if len(words) != SEGMENT_WORDS:
  835. raise ValueError("第 %d 段回读长度错误" % (index + 1))
  836. variables = self.segment_vars[index]
  837. frequency = words[0] | (words[1] << 16)
  838. pulse_bits = words[2] | (words[3] << 16)
  839. pulses = pulse_bits - (1 << 32) if pulse_bits & 0x80000000 else pulse_bits
  840. variables["frequency"].set(str(frequency))
  841. variables["pulses"].set(str(pulses))
  842. variables["wait_type"].set(option_label(WAIT_OPTIONS, words[4]))
  843. variables["wait_ms"].set(str(words[5]))
  844. variables["act_ms"].set(str(words[6]))
  845. variables["jump"].set(str(words[7]))
  846. def _read_all(self):
  847. if self._start_operation("正在读取公共参数和 10 段参数"):
  848. self.worker.submit("read_configuration", operation="读取参数")
  849. def _write_common(self):
  850. try:
  851. words = self._common_words()
  852. output_mode = option_value(
  853. OUTPUT_MODE_OPTIONS,
  854. self.common_vars["output_mode"].get(),
  855. "输出模式",
  856. )
  857. if output_mode == OUTPUT_AB and words[0] not in (0, 2):
  858. raise ValueError("AB 模式的脉冲输出只能选择 Y0/Y1 或 Y2/Y3")
  859. except ValueError as error:
  860. messagebox.showerror("参数错误", str(error))
  861. return
  862. if self._start_operation("正在写入公共参数"):
  863. self.worker.submit(
  864. "write_common",
  865. words=words,
  866. output_mode=output_mode,
  867. operation="写入公共参数",
  868. )
  869. def _write_segments(self):
  870. try:
  871. segments = self._segment_words()
  872. except ValueError as error:
  873. messagebox.showerror("参数错误", str(error))
  874. return
  875. if self._start_operation("正在写入 10 段参数"):
  876. self.worker.submit(
  877. "write_segments", segments=segments, operation="写入段参数"
  878. )
  879. def _send_control(self, command):
  880. messages = {
  881. COMMAND_START: "启动命令已发送",
  882. COMMAND_STOP: "停止命令已发送",
  883. COMMAND_CLEAR: "清零命令已发送",
  884. }
  885. if self._start_operation("正在发送控制命令"):
  886. self.worker.submit(
  887. "control",
  888. value=command,
  889. message=messages[command],
  890. operation="控制命令",
  891. )
  892. def _clear_position(self):
  893. if messagebox.askyesno("确认清零", "确认将累计位置清零?"):
  894. self._send_control(COMMAND_CLEAR)
  895. def _clear_diagnostic(self):
  896. if self._start_operation("正在清除诊断锁存和统计"):
  897. self.worker.submit(
  898. "clear_diagnostic", operation="清除诊断"
  899. )
  900. def _show_status(self, status):
  901. self.status_vars["position"].set(str(status.position))
  902. self.status_vars["frequency"].set("%d Hz" % status.frequency_hz)
  903. self.status_vars["state"].set(
  904. "%s (%d)" % (STATUS_NAMES.get(status.state, "未知"), status.state)
  905. )
  906. self.status_vars["segment"].set(str(status.segment))
  907. self.status_vars["error"].set(
  908. "%s (%d)" % (ERROR_NAMES.get(status.error, "未知"), status.error)
  909. )
  910. def _show_diagnostic(self, diagnostic):
  911. def check_name(checked_bit, pass_bit):
  912. if not diagnostic.flags & checked_bit:
  913. return "未检查"
  914. return "通过" if diagnostic.flags & pass_bit else "失败"
  915. checks = "计数 %s / 频率 %s / 曲线 %s" % (
  916. check_name(DIAG_COUNT_CHECKED, DIAG_COUNT_PASS),
  917. check_name(DIAG_FREQUENCY_CHECKED, DIAG_FREQUENCY_PASS),
  918. check_name(DIAG_CURVE_CHECKED, DIAG_CURVE_PASS),
  919. )
  920. if diagnostic.flags & DIAG_MONITORING:
  921. checks = "监测中 / " + checks
  922. if diagnostic.flags & DIAG_FAULT_LATCHED:
  923. checks = "故障锁存 / " + checks
  924. direction = "正向" if diagnostic.direction_positive else "反向"
  925. mode = "AB" if diagnostic.output_mode == OUTPUT_AB else "PULSE/DIR"
  926. first_mismatch = (
  927. "无" if diagnostic.first_mismatch_sample == 0xFFFFFFFF
  928. else str(diagnostic.first_mismatch_sample)
  929. )
  930. values = self.diagnostic_vars
  931. values["summary"].set(checks)
  932. values["reason"].set(
  933. "%s (%d)" % (
  934. DIAGNOSTIC_REASON_NAMES.get(diagnostic.reason, "未知"),
  935. diagnostic.reason,
  936. )
  937. )
  938. values["segment"].set(str(diagnostic.segment))
  939. values["mode_direction"].set("%s / %s" % (mode, direction))
  940. values["expected_count"].set(str(diagnostic.expected_pulses))
  941. values["actual_count"].set(str(diagnostic.actual_pulses))
  942. values["count_error"].set(str(diagnostic.count_error))
  943. values["requested_hz"].set("%d Hz" % diagnostic.requested_hz)
  944. values["expected_hz"].set("%d Hz" % diagnostic.expected_timer_hz)
  945. values["active_hz"].set("%d Hz" % diagnostic.active_timer_hz)
  946. values["request_error"].set("%d Hz" % diagnostic.request_error_hz)
  947. values["sample_count"].set(str(diagnostic.sample_count))
  948. values["mismatch_count"].set(str(diagnostic.mismatch_count))
  949. values["maximum_error"].set(
  950. "%d Hz" % diagnostic.maximum_frequency_error_hz
  951. )
  952. values["first_mismatch"].set(first_mismatch)
  953. def _handle_result(self, event, payload):
  954. if event == "connection":
  955. if payload["connected"]:
  956. description = "%s / 从站 %d" % (payload["port"], payload["slave"])
  957. self._set_connected(True, description)
  958. self.footer_var.set("已连接,正在读取参数")
  959. else:
  960. reason = payload.get("reason", "")
  961. self.operation_pending = False
  962. self.connect_button.configure(state="normal")
  963. self._set_connected(False, reason)
  964. self.footer_var.set("连接已断开" if not reason else "通信错误")
  965. elif event == "configuration":
  966. self._set_common_words(payload["common"])
  967. self.common_vars["output_mode"].set(
  968. option_label(OUTPUT_MODE_OPTIONS, payload["output_mode"])
  969. )
  970. self._set_segment_words(payload["segments"])
  971. self._finish_operation("全部参数已读取")
  972. elif event == "common":
  973. self._set_common_words(payload["words"])
  974. self.common_vars["output_mode"].set(
  975. option_label(OUTPUT_MODE_OPTIONS, payload["output_mode"])
  976. )
  977. elif event == "segments":
  978. self._set_segment_words(payload["words"])
  979. elif event == "status":
  980. self._show_status(payload["status"])
  981. elif event == "diagnostic":
  982. self._show_diagnostic(payload["diagnostic"])
  983. elif event == "log":
  984. if self.log_text is not None and self.log_text.winfo_exists():
  985. self.log_text.configure(state="normal")
  986. self.log_text.insert("end", payload["message"] + "\n")
  987. self.log_text.see("end")
  988. self.log_text.configure(state="disabled")
  989. elif event == "operation":
  990. self._finish_operation(payload["message"])
  991. elif event == "error":
  992. message = "%s失败:%s" % (payload["operation"], payload["message"])
  993. self._finish_operation(message)
  994. self.footer_label.configure(style="Error.TLabel")
  995. if payload.get("modal", False):
  996. messagebox.showerror("PLSR 通信错误", message)
  997. def _drain_results(self):
  998. try:
  999. while True:
  1000. event, payload = self.worker.results.get_nowait()
  1001. try:
  1002. self._handle_result(event, payload)
  1003. except (KeyError, ValueError) as error:
  1004. self._finish_operation("界面数据错误:%s" % error)
  1005. except queue.Empty:
  1006. pass
  1007. self.root.after(50, self._drain_results)
  1008. def _on_close(self):
  1009. self.worker.close()
  1010. self.root.destroy()
  1011. class _SelfTestVariable:
  1012. def __init__(self, value):
  1013. self.value = str(value)
  1014. def get(self):
  1015. return self.value
  1016. def set(self, value):
  1017. self.value = str(value)
  1018. class _SelfTestClient:
  1019. def __init__(self):
  1020. self.calls = []
  1021. def write_single(self, address, value):
  1022. self.calls.append(("single", address, value))
  1023. def write_multiple(self, address, values):
  1024. self.calls.append(("multiple", address, list(values)))
  1025. def self_test():
  1026. assert BAUD_RATE == 9600
  1027. assert INPUT_OPTIONS == (("X4 (0)", 0), ("X5 (1)", 1))
  1028. assert [SEGMENT_1_BASE + index * SEGMENT_STRIDE
  1029. for index in range(SEGMENT_COUNT)] == [
  1030. 0x1100, 0x1110, 0x1120, 0x1130, 0x1140,
  1031. 0x1150, 0x1160, 0x1170, 0x1180, 0x1190,
  1032. ]
  1033. assert parse_integer("0x10", "测试值", 0, 16) == 16
  1034. try:
  1035. parse_integer("17", "测试值", 0, 16)
  1036. except ValueError:
  1037. pass
  1038. else:
  1039. raise AssertionError("range validation did not reject 17")
  1040. panel = PlsrControlPanel.__new__(PlsrControlPanel)
  1041. panel.common_vars = {
  1042. "pulse_output": _SelfTestVariable(PULSE_OPTIONS[3][0]),
  1043. "direction_output": _SelfTestVariable(DIRECTION_OPTIONS[3][0]),
  1044. "wait_input": _SelfTestVariable(INPUT_OPTIONS[1][0]),
  1045. "ext_input": _SelfTestVariable(INPUT_OPTIONS[0][0]),
  1046. "send_mode": _SelfTestVariable(SEND_OPTIONS[1][0]),
  1047. "direction_delay": _SelfTestVariable(25),
  1048. "negative_logic": _SelfTestVariable(LOGIC_OPTIONS[1][0]),
  1049. "curve_mode": _SelfTestVariable(CURVE_OPTIONS[2][0]),
  1050. "position_mode": _SelfTestVariable(POSITION_OPTIONS[1][0]),
  1051. "segment_count": _SelfTestVariable(10),
  1052. "start_segment": _SelfTestVariable(2),
  1053. "default_speed": _SelfTestVariable(100000),
  1054. "start_speed": _SelfTestVariable(0),
  1055. "stop_speed": _SelfTestVariable(65536),
  1056. "acceleration": _SelfTestVariable(1000),
  1057. "deceleration": _SelfTestVariable(2000),
  1058. "output_mode": _SelfTestVariable(OUTPUT_MODE_OPTIONS[1][0]),
  1059. }
  1060. common = panel._common_words()
  1061. assert common[:11] == [3, 3, 1, 0, 1, 25, 1, 2, 1, 10, 2]
  1062. assert common[11:20] == [34464, 1, 0, 0, 0, 0, 1, 1000, 2000]
  1063. assert option_value(
  1064. OUTPUT_MODE_OPTIONS,
  1065. panel.common_vars["output_mode"].get(),
  1066. "输出模式",
  1067. ) == OUTPUT_AB
  1068. assert OUTPUT_MODE == 0x1200
  1069. assert DIAGNOSTIC_CONTROL == 0x3100
  1070. client = _SelfTestClient()
  1071. write_common_configuration(client, common, OUTPUT_PULSE_DIR)
  1072. assert client.calls[0] == ("single", OUTPUT_MODE, OUTPUT_PULSE_DIR)
  1073. assert client.calls[1][0:2] == ("multiple", CONFIG_BASE)
  1074. client = _SelfTestClient()
  1075. write_common_configuration(client, common, OUTPUT_AB)
  1076. assert client.calls[0][0:2] == ("multiple", CONFIG_BASE)
  1077. assert client.calls[1] == ("single", OUTPUT_MODE, OUTPUT_AB)
  1078. panel.segment_vars = []
  1079. for index in range(SEGMENT_COUNT):
  1080. panel.segment_vars.append({
  1081. "frequency": _SelfTestVariable(1000 + index),
  1082. "pulses": _SelfTestVariable(-1 if index == 0 else index),
  1083. "wait_type": _SelfTestVariable(WAIT_OPTIONS[4][0]),
  1084. "wait_ms": _SelfTestVariable(index),
  1085. "act_ms": _SelfTestVariable(index + 1),
  1086. "jump": _SelfTestVariable(0),
  1087. })
  1088. segments = panel._segment_words()
  1089. assert len(segments) == SEGMENT_COUNT
  1090. assert all(len(words) == SEGMENT_WORDS for words in segments)
  1091. assert segments[0] == [1000, 0, 0xFFFF, 0xFFFF, 4, 0, 1, 0]
  1092. panel.segment_vars[0]["frequency"].set(0)
  1093. segments = panel._segment_words()
  1094. assert segments[0][0:2] == [0, 0]
  1095. panel.common_vars["segment_count"].set(1)
  1096. panel.segment_vars[0]["jump"].set(2)
  1097. try:
  1098. panel._segment_words()
  1099. except ValueError:
  1100. pass
  1101. else:
  1102. raise AssertionError("active segment jump exceeded segment count")
  1103. print("PLSR control panel self-test passed; no GUI or serial port was opened")
  1104. def parse_arguments(arguments=None):
  1105. parser = argparse.ArgumentParser(description=__doc__)
  1106. parser.add_argument(
  1107. "--self-test",
  1108. action="store_true",
  1109. help="validate register encoding without opening the GUI or serial port",
  1110. )
  1111. return parser.parse_args(arguments)
  1112. def main(arguments=None):
  1113. args = parse_arguments(arguments)
  1114. if args.self_test:
  1115. self_test()
  1116. return 0
  1117. root = tk.Tk()
  1118. PlsrControlPanel(root)
  1119. root.mainloop()
  1120. return 0
  1121. if __name__ == "__main__":
  1122. raise SystemExit(main())