|
- """Tkinter control panel for the 2026 PLSR Modbus register map."""
-
- from __future__ import annotations
-
- import argparse
- import queue
- import threading
- import time
- import tkinter as tk
- import tkinter.font as tkfont
- from tkinter import messagebox, ttk
-
- import serial
- from serial.tools import list_ports
-
- # Keep one RTU implementation for the board tests and this operator panel.
- from plsr_modbus_product_test import (
- COMMAND_CLEAR,
- COMMAND_START,
- COMMAND_STOP,
- COMMON_WORDS,
- CONFIG_BASE,
- CONTROL,
- ModbusException,
- RtuClient,
- SEGMENT_1_BASE,
- SEGMENT_WORDS,
- TestFailure,
- read_status,
- split_i32,
- split_u32,
- )
-
-
- BAUD_RATE = 9600
- SERIAL_TIMEOUT_SECONDS = 0.8
- STATUS_POLL_SECONDS = 0.25
- SEGMENT_COUNT = 10
- SEGMENT_STRIDE = 0x10
- MAX_FREQUENCY_HZ = 100_000
-
- PULSE_OPTIONS = (
- ("Y0 (0)", 0),
- ("Y1 (1)", 1),
- ("Y2 (2)", 2),
- ("Y3 (3)", 3),
- )
- DIRECTION_OPTIONS = (
- ("Y12 (0)", 0),
- ("Y13 (1)", 1),
- ("Y14 (2)", 2),
- ("Y15 (3)", 3),
- )
- INPUT_OPTIONS = (("X4 (0)", 0), ("X5 (1)", 1))
- SEND_OPTIONS = (("脉冲发送完毕 (0)", 0), ("后续段 (1)", 1))
- LOGIC_OPTIONS = (("正逻辑 (0)", 0), ("负逻辑 (1)", 1))
- CURVE_OPTIONS = (("直线 (0)", 0), ("S 曲线 (1)", 1), ("正弦曲线 (2)", 2))
- POSITION_OPTIONS = (("相对位置 (0)", 0), ("绝对位置 (1)", 1))
- WAIT_OPTIONS = (
- ("WAIT 时间 (0)", 0),
- ("WAIT 信号 (1)", 1),
- ("ACT 时间 (2)", 2),
- ("EXT 信号 (3)", 3),
- ("EXT 或完成 (4)", 4),
- )
-
- STATUS_NAMES = {
- 0: "未初始化",
- 1: "空闲",
- 2: "加速",
- 3: "运行",
- 4: "减速",
- 5: "等待",
- 6: "暂停",
- 7: "完成",
- 8: "停止",
- 9: "错误",
- }
- ERROR_NAMES = {
- 0: "无错误",
- 1: "状态转换非法",
- 2: "资源冲突",
- 3: "资源非法",
- 4: "定时器错误",
- 5: "计数错误",
- 6: "正限位",
- 7: "负限位",
- 8: "急停",
- 9: "内部错误",
- }
-
-
- def option_labels(options):
- return tuple(label for label, _value in options)
-
-
- def option_label(options, value):
- for label, candidate in options:
- if candidate == value:
- return label
- return str(value)
-
-
- def option_value(options, label, field_name):
- for candidate_label, value in options:
- if candidate_label == label:
- return value
- raise ValueError("%s 不是有效选项" % field_name)
-
-
- def parse_integer(text, field_name, minimum, maximum):
- raw = text.strip()
- if not raw:
- raise ValueError("%s 不能为空" % field_name)
- try:
- value = int(raw, 0)
- except ValueError:
- try:
- value = int(raw, 10)
- except ValueError as error:
- raise ValueError("%s 必须是整数" % field_name) from error
- if value < minimum or value > maximum:
- raise ValueError(
- "%s 必须在 %d 到 %d 之间" % (field_name, minimum, maximum)
- )
- return value
-
-
- class SerialWorker(threading.Thread):
- """Own the serial port and report all results through a queue."""
-
- def __init__(self):
- super().__init__(name="plsr-serial", daemon=True)
- self.commands = queue.Queue()
- self.results = queue.Queue()
- self.stop_event = threading.Event()
- self.client = None
- self.next_status_poll = 0.0
-
- def submit(self, command, **payload):
- self.commands.put((command, payload))
-
- def close(self):
- self.stop_event.set()
- self.commands.put(("shutdown", {}))
-
- def _emit(self, event, **payload):
- self.results.put((event, payload))
-
- def _disconnect(self, notify=True, reason=""):
- client = self.client
- self.client = None
- if client is not None:
- try:
- client.__exit__(None, None, None)
- except (OSError, serial.SerialException):
- pass
- if notify:
- self._emit("connection", connected=False, reason=reason)
-
- def _require_client(self):
- if self.client is None:
- raise RuntimeError("串口尚未连接")
- return self.client
-
- def _read_configuration(self):
- client = self._require_client()
- common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
- segments = []
- for index in range(SEGMENT_COUNT):
- address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
- segments.append(client.read_holding(address, SEGMENT_WORDS))
- self._emit("configuration", common=common, segments=segments)
-
- def _handle_connection_error(self, operation, error):
- self._disconnect(notify=True, reason=str(error))
- self._emit("error", operation=operation, message=str(error), modal=True)
-
- def _handle_command(self, command, payload):
- if command == "connect":
- self._disconnect(notify=False)
- try:
- client = RtuClient(
- payload["port"],
- BAUD_RATE,
- payload["slave"],
- SERIAL_TIMEOUT_SECONDS,
- )
- client.__enter__()
- self.client = client
- self.next_status_poll = 0.0
- self._emit(
- "connection",
- connected=True,
- port=payload["port"],
- slave=payload["slave"],
- )
- self._read_configuration()
- except (OSError, serial.SerialException) as error:
- self._handle_connection_error("连接", error)
- except (TestFailure, ModbusException, RuntimeError) as error:
- self._disconnect(notify=True, reason=str(error))
- self._emit(
- "error", operation="读取参数", message=str(error), modal=True
- )
- return
-
- if command == "disconnect":
- self._disconnect(notify=True)
- return
-
- try:
- client = self._require_client()
- if command == "read_configuration":
- self._read_configuration()
- self._emit("operation", message="参数读取完成")
- elif command == "write_common":
- client.write_multiple(CONFIG_BASE, payload["words"])
- common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
- self._emit("common", words=common)
- self._emit("operation", message="公共参数写入并回读完成")
- elif command == "write_segments":
- for index, words in enumerate(payload["segments"]):
- address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
- client.write_multiple(address, words)
- segments = []
- for index in range(SEGMENT_COUNT):
- address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
- segments.append(client.read_holding(address, SEGMENT_WORDS))
- self._emit("segments", words=segments)
- self._emit("operation", message="10 段参数写入并回读完成")
- elif command == "control":
- client.write_single(CONTROL, payload["value"])
- self._emit("operation", message=payload["message"])
- else:
- raise RuntimeError("未知后台命令: %s" % command)
- except (OSError, serial.SerialException) as error:
- self._handle_connection_error(payload.get("operation", "通信"), error)
- except (TestFailure, ModbusException, RuntimeError) as error:
- self._emit(
- "error",
- operation=payload.get("operation", "通信"),
- message=str(error),
- modal=True,
- )
-
- def _poll_status(self):
- try:
- status = read_status(self._require_client())
- self._emit("status", status=status)
- self.next_status_poll = time.monotonic() + STATUS_POLL_SECONDS
- except (OSError, serial.SerialException) as error:
- self._handle_connection_error("状态轮询", error)
- except (TestFailure, ModbusException, RuntimeError) as error:
- self._emit(
- "error", operation="状态轮询", message=str(error), modal=False
- )
- self.next_status_poll = time.monotonic() + 1.0
-
- def run(self):
- try:
- while not self.stop_event.is_set():
- try:
- command, payload = self.commands.get(timeout=0.05)
- except queue.Empty:
- command = None
- payload = None
-
- if command == "shutdown":
- break
- if command is not None:
- self._handle_command(command, payload)
-
- if (
- self.client is not None
- and time.monotonic() >= self.next_status_poll
- ):
- self._poll_status()
- finally:
- self._disconnect(notify=False)
-
-
- class PlsrControlPanel:
- def __init__(self, root):
- self.root = root
- self.root.title("PLSR 控制面板")
- self.root.geometry("1180x720")
- self.root.minsize(980, 650)
- self.root.protocol("WM_DELETE_WINDOW", self._on_close)
-
- self.connected = False
- self.operation_pending = False
- self.worker = SerialWorker()
- self.worker.start()
-
- self.port_var = tk.StringVar()
- self.slave_var = tk.StringVar(value="1")
- self.connection_var = tk.StringVar(value="未连接")
- self.footer_var = tk.StringVar(value="请选择串口并连接")
- self.status_vars = {
- "position": tk.StringVar(value="--"),
- "frequency": tk.StringVar(value="--"),
- "state": tk.StringVar(value="--"),
- "segment": tk.StringVar(value="--"),
- "error": tk.StringVar(value="--"),
- }
- self.common_vars = {}
- self.segment_vars = []
- self.connection_widgets = []
- self.action_buttons = []
-
- self._configure_style()
- self._build_ui()
- self.refresh_ports()
- self._set_connected(False)
- self.root.after(50, self._drain_results)
-
- def _configure_style(self):
- default_font = tkfont.nametofont("TkDefaultFont")
- default_font.configure(family="Microsoft YaHei UI", size=10)
- text_font = tkfont.nametofont("TkTextFont")
- text_font.configure(family="Microsoft YaHei UI", size=10)
- style = ttk.Style(self.root)
- style.configure("Connected.TLabel", foreground="#18794e")
- style.configure("Disconnected.TLabel", foreground="#5f6368")
- style.configure("Error.TLabel", foreground="#b42318")
- style.configure("StatusValue.TLabel", font=("Microsoft YaHei UI", 11, "bold"))
-
- def _build_ui(self):
- self.root.columnconfigure(0, weight=1)
- self.root.rowconfigure(2, weight=1)
-
- connection = ttk.LabelFrame(self.root, text="连接")
- connection.grid(row=0, column=0, padx=10, pady=(10, 6), sticky="ew")
- connection.columnconfigure(7, weight=1)
-
- ttk.Label(connection, text="串口").grid(row=0, column=0, padx=(8, 4), pady=8)
- self.port_box = ttk.Combobox(
- connection, textvariable=self.port_var, width=13, state="readonly"
- )
- self.port_box.grid(row=0, column=1, padx=4, pady=8)
- refresh_button = ttk.Button(connection, text="刷新", command=self.refresh_ports)
- refresh_button.grid(row=0, column=2, padx=(0, 12), pady=8)
- ttk.Label(connection, text="串口参数").grid(row=0, column=3, padx=4, pady=8)
- ttk.Label(connection, text="9600, 8E1").grid(row=0, column=4, padx=(4, 12), pady=8)
- ttk.Label(connection, text="从站地址").grid(row=0, column=5, padx=4, pady=8)
- slave_entry = ttk.Entry(connection, textvariable=self.slave_var, width=7)
- slave_entry.grid(row=0, column=6, padx=(4, 12), pady=8)
- self.connect_button = ttk.Button(
- connection, text="连接", command=self._toggle_connection
- )
- self.connect_button.grid(row=0, column=8, padx=8, pady=8)
- self.connection_label = ttk.Label(
- connection,
- textvariable=self.connection_var,
- style="Disconnected.TLabel",
- width=28,
- anchor="e",
- )
- self.connection_label.grid(row=0, column=7, padx=8, pady=8, sticky="e")
- self.connection_widgets = [self.port_box, refresh_button, slave_entry]
-
- self._build_runtime_status()
-
- notebook = ttk.Notebook(self.root)
- notebook.grid(row=2, column=0, padx=10, pady=6, sticky="nsew")
- common_tab = ttk.Frame(notebook, padding=12)
- segment_tab = ttk.Frame(notebook, padding=12)
- notebook.add(common_tab, text="公共参数 0x1000-0x1013")
- notebook.add(segment_tab, text="10 段参数 0x1100-0x1197")
- self._build_common_tab(common_tab)
- self._build_segment_tab(segment_tab)
-
- footer = ttk.Frame(self.root)
- footer.grid(row=3, column=0, padx=10, pady=(4, 10), sticky="ew")
- footer.columnconfigure(4, weight=1)
- read_button = ttk.Button(footer, text="读取全部参数", command=self._read_all)
- read_button.grid(row=0, column=0, padx=(0, 6))
- common_button = ttk.Button(
- footer, text="写入公共参数", command=self._write_common
- )
- common_button.grid(row=0, column=1, padx=6)
- segment_button = ttk.Button(
- footer, text="写入 10 段参数", command=self._write_segments
- )
- segment_button.grid(row=0, column=2, padx=6)
- self.action_buttons.extend([read_button, common_button, segment_button])
- self.footer_label = ttk.Label(
- footer, textvariable=self.footer_var, anchor="e"
- )
- self.footer_label.grid(row=0, column=4, padx=(12, 0), sticky="ew")
-
- def _build_runtime_status(self):
- frame = ttk.LabelFrame(self.root, text="实时状态 0x2000-0x2006")
- frame.grid(row=1, column=0, padx=10, pady=6, sticky="ew")
- for column in range(11):
- frame.columnconfigure(column, weight=1 if column % 2 else 0)
-
- fields = (
- ("累计位置", "position"),
- ("当前频率", "frequency"),
- ("运行状态", "state"),
- ("当前段", "segment"),
- ("错误码", "error"),
- )
- for index, (label, key) in enumerate(fields):
- ttk.Label(frame, text=label).grid(
- row=0, column=index * 2, padx=(8, 4), pady=9, sticky="e"
- )
- ttk.Label(
- frame, textvariable=self.status_vars[key], style="StatusValue.TLabel"
- ).grid(row=0, column=index * 2 + 1, padx=(4, 12), pady=9, sticky="w")
-
- commands = ttk.Frame(frame)
- commands.grid(row=0, column=10, padx=8, pady=6, sticky="e")
- start_button = ttk.Button(
- commands, text="启动", command=lambda: self._send_control(COMMAND_START)
- )
- stop_button = ttk.Button(
- commands, text="停止", command=lambda: self._send_control(COMMAND_STOP)
- )
- clear_button = ttk.Button(commands, text="清零", command=self._clear_position)
- start_button.grid(row=0, column=0, padx=3)
- stop_button.grid(row=0, column=1, padx=3)
- clear_button.grid(row=0, column=2, padx=3)
- self.action_buttons.extend([start_button, stop_button, clear_button])
-
- def _add_entry(self, parent, row, group, key, label, default="0", width=13):
- column = group * 2
- ttk.Label(parent, text=label).grid(
- row=row, column=column, padx=(8, 4), pady=7, sticky="e"
- )
- variable = tk.StringVar(value=default)
- ttk.Entry(parent, textvariable=variable, width=width).grid(
- row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w"
- )
- self.common_vars[key] = variable
-
- def _add_combo(self, parent, row, group, key, label, options):
- column = group * 2
- ttk.Label(parent, text=label).grid(
- row=row, column=column, padx=(8, 4), pady=7, sticky="e"
- )
- variable = tk.StringVar(value=options[0][0])
- ttk.Combobox(
- parent,
- textvariable=variable,
- values=option_labels(options),
- state="readonly",
- width=18,
- ).grid(row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w")
- self.common_vars[key] = variable
-
- def _build_common_tab(self, parent):
- for column in (1, 3, 5):
- parent.columnconfigure(column, weight=1)
-
- self._add_combo(
- parent, 0, 0, "pulse_output", "脉冲输出(单逻辑 PLSR)", PULSE_OPTIONS
- )
- self._add_combo(
- parent, 0, 1, "direction_output", "方向输出", DIRECTION_OPTIONS
- )
- self._add_entry(parent, 0, 2, "direction_delay", "方向延时 ms")
-
- self._add_combo(parent, 1, 0, "wait_input", "WAIT 输入", INPUT_OPTIONS)
- self._add_combo(parent, 1, 1, "ext_input", "EXT 输入", INPUT_OPTIONS)
- self._add_combo(parent, 1, 2, "send_mode", "发送模式", SEND_OPTIONS)
-
- self._add_combo(parent, 2, 0, "negative_logic", "方向逻辑", LOGIC_OPTIONS)
- self._add_combo(parent, 2, 1, "curve_mode", "曲线模式", CURVE_OPTIONS)
- self._add_combo(parent, 2, 2, "position_mode", "位置模式", POSITION_OPTIONS)
-
- self._add_entry(parent, 3, 0, "segment_count", "段数", default="1")
- self._add_entry(parent, 3, 1, "start_segment", "起始段", default="1")
- self._add_entry(parent, 3, 2, "default_speed", "基准速度 Hz", default="1000")
-
- self._add_entry(parent, 4, 0, "start_speed", "启动速度 Hz", default="100")
- self._add_entry(parent, 4, 1, "stop_speed", "停止速度 Hz", default="100")
- self._add_entry(parent, 4, 2, "acceleration", "加速时间 ms")
-
- self._add_entry(parent, 5, 0, "deceleration", "减速时间 ms")
-
- def _build_segment_tab(self, parent):
- headers = (
- ("段 / 基址", 15),
- ("频率 Hz", 13),
- ("脉冲数", 15),
- ("等待类型", 19),
- ("WAIT ms", 11),
- ("ACT ms", 11),
- ("跳转段", 9),
- )
- for column, (label, _width) in enumerate(headers):
- parent.columnconfigure(column, weight=1 if column in (1, 2, 3) else 0)
- ttk.Label(parent, text=label, anchor="center").grid(
- row=0, column=column, padx=4, pady=(0, 6), sticky="ew"
- )
-
- for index in range(SEGMENT_COUNT):
- address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
- ttk.Label(parent, text="%d / 0x%04X" % (index + 1, address)).grid(
- row=index + 1, column=0, padx=(2, 8), pady=4, sticky="e"
- )
- variables = {
- "frequency": tk.StringVar(value="1000"),
- "pulses": tk.StringVar(value="0"),
- "wait_type": tk.StringVar(value=WAIT_OPTIONS[0][0]),
- "wait_ms": tk.StringVar(value="0"),
- "act_ms": tk.StringVar(value="0"),
- "jump": tk.StringVar(value="0"),
- }
- ttk.Entry(parent, textvariable=variables["frequency"], width=13).grid(
- row=index + 1, column=1, padx=4, pady=4, sticky="ew"
- )
- ttk.Entry(parent, textvariable=variables["pulses"], width=15).grid(
- row=index + 1, column=2, padx=4, pady=4, sticky="ew"
- )
- ttk.Combobox(
- parent,
- textvariable=variables["wait_type"],
- values=option_labels(WAIT_OPTIONS),
- state="readonly",
- width=19,
- ).grid(row=index + 1, column=3, padx=4, pady=4, sticky="ew")
- ttk.Entry(parent, textvariable=variables["wait_ms"], width=11).grid(
- row=index + 1, column=4, padx=4, pady=4
- )
- ttk.Entry(parent, textvariable=variables["act_ms"], width=11).grid(
- row=index + 1, column=5, padx=4, pady=4
- )
- ttk.Entry(parent, textvariable=variables["jump"], width=9).grid(
- row=index + 1, column=6, padx=4, pady=4
- )
- self.segment_vars.append(variables)
-
- def refresh_ports(self):
- ports = [item.device for item in list_ports.comports()]
- self.port_box["values"] = ports
- if ports and self.port_var.get() not in ports:
- self.port_var.set(ports[0])
- elif not ports:
- self.port_var.set("")
-
- def _set_connected(self, connected, description=""):
- self.connected = connected
- self.connect_button.configure(
- text="断开" if connected else "连接", state="normal"
- )
- connection_state = "disabled" if connected else "normal"
- for widget in self.connection_widgets:
- if widget is self.port_box:
- widget.configure(state="disabled" if connected else "readonly")
- else:
- widget.configure(state=connection_state)
- if connected:
- self.connection_var.set(description or "已连接")
- self.connection_label.configure(style="Connected.TLabel")
- else:
- self.connection_var.set(description or "未连接")
- self.connection_label.configure(
- style="Error.TLabel" if description else "Disconnected.TLabel"
- )
- for variable in self.status_vars.values():
- variable.set("--")
- self._update_action_state()
-
- def _update_action_state(self):
- state = "normal" if self.connected and not self.operation_pending else "disabled"
- for button in self.action_buttons:
- button.configure(state=state)
-
- def _start_operation(self, message):
- if not self.connected:
- messagebox.showerror("未连接", "请先连接 PLSR 从站")
- return False
- if self.operation_pending:
- return False
- self.operation_pending = True
- self.footer_var.set(message)
- self._update_action_state()
- return True
-
- def _finish_operation(self, message):
- self.operation_pending = False
- self.footer_var.set(message)
- self.footer_label.configure(style="TLabel")
- self._update_action_state()
-
- def _toggle_connection(self):
- if self.connected:
- self.operation_pending = False
- self.footer_var.set("正在断开")
- self.worker.submit("disconnect")
- return
- port = self.port_var.get().strip()
- if not port:
- messagebox.showerror("连接参数", "请选择可用串口")
- return
- try:
- slave = parse_integer(self.slave_var.get(), "从站地址", 1, 247)
- except ValueError as error:
- messagebox.showerror("连接参数", str(error))
- return
- self.connect_button.configure(state="disabled")
- self.operation_pending = True
- self._update_action_state()
- self.connection_var.set("正在连接 %s" % port)
- self.footer_var.set("正在打开串口并读取参数")
- self.worker.submit("connect", port=port, slave=slave)
-
- def _common_words(self):
- values = self.common_vars
- segment_count = parse_integer(values["segment_count"].get(), "段数", 1, 10)
- start_segment = parse_integer(
- values["start_segment"].get(), "起始段", 1, 10
- )
- if start_segment > segment_count:
- raise ValueError("起始段不能大于段数")
-
- words = [0] * COMMON_WORDS
- words[0] = option_value(
- PULSE_OPTIONS, values["pulse_output"].get(), "脉冲输出"
- )
- words[1] = option_value(
- DIRECTION_OPTIONS, values["direction_output"].get(), "方向输出"
- )
- words[2] = option_value(INPUT_OPTIONS, values["wait_input"].get(), "WAIT 输入")
- words[3] = option_value(INPUT_OPTIONS, values["ext_input"].get(), "EXT 输入")
- words[4] = option_value(SEND_OPTIONS, values["send_mode"].get(), "发送模式")
- words[5] = parse_integer(
- values["direction_delay"].get(), "方向延时", 0, 0xFFFF
- )
- words[6] = option_value(
- LOGIC_OPTIONS, values["negative_logic"].get(), "方向逻辑"
- )
- words[7] = option_value(CURVE_OPTIONS, values["curve_mode"].get(), "曲线模式")
- words[8] = option_value(
- POSITION_OPTIONS, values["position_mode"].get(), "位置模式"
- )
- words[9] = segment_count
- words[10] = start_segment
- words[11:13] = split_u32(
- parse_integer(
- values["default_speed"].get(), "基准速度", 1, MAX_FREQUENCY_HZ
- )
- )
- words[13:15] = split_u32(
- parse_integer(
- values["start_speed"].get(), "启动速度", 0, MAX_FREQUENCY_HZ
- )
- )
- words[15] = 0
- words[16:18] = split_u32(
- parse_integer(
- values["stop_speed"].get(), "停止速度", 0, MAX_FREQUENCY_HZ
- )
- )
- words[18] = parse_integer(values["acceleration"].get(), "加速时间", 0, 0xFFFF)
- words[19] = parse_integer(values["deceleration"].get(), "减速时间", 0, 0xFFFF)
- return words
-
- def _segment_words(self):
- segment_count = parse_integer(
- self.common_vars["segment_count"].get(), "段数", 1, SEGMENT_COUNT
- )
- encoded = []
- for index, variables in enumerate(self.segment_vars):
- name = "第 %d 段" % (index + 1)
- frequency = parse_integer(
- variables["frequency"].get(), name + "频率", 1, MAX_FREQUENCY_HZ
- )
- pulses = parse_integer(
- variables["pulses"].get(), name + "脉冲数", -(1 << 31), (1 << 31) - 1
- )
- wait_type = option_value(
- WAIT_OPTIONS, variables["wait_type"].get(), name + "等待类型"
- )
- wait_ms = parse_integer(
- variables["wait_ms"].get(), name + " WAIT 时间", 0, 0xFFFF
- )
- act_ms = parse_integer(
- variables["act_ms"].get(), name + " ACT 时间", 0, 0xFFFF
- )
- jump = parse_integer(variables["jump"].get(), name + "跳转段", 0, 10)
- if index < segment_count and jump > segment_count:
- raise ValueError(
- "%s跳转段必须为 0 或不大于脉冲总段数 %d"
- % (name, segment_count)
- )
- words = split_u32(frequency) + split_i32(pulses)
- words.extend((wait_type, wait_ms, act_ms, jump))
- encoded.append(words)
- return encoded
-
- def _set_common_words(self, words):
- if len(words) != COMMON_WORDS:
- raise ValueError("公共参数回读长度错误")
- values = self.common_vars
- values["pulse_output"].set(option_label(PULSE_OPTIONS, words[0]))
- values["direction_output"].set(option_label(DIRECTION_OPTIONS, words[1]))
- values["wait_input"].set(option_label(INPUT_OPTIONS, words[2]))
- values["ext_input"].set(option_label(INPUT_OPTIONS, words[3]))
- values["send_mode"].set(option_label(SEND_OPTIONS, words[4]))
- values["direction_delay"].set(str(words[5]))
- values["negative_logic"].set(option_label(LOGIC_OPTIONS, words[6]))
- values["curve_mode"].set(option_label(CURVE_OPTIONS, words[7]))
- values["position_mode"].set(option_label(POSITION_OPTIONS, words[8]))
- values["segment_count"].set(str(words[9]))
- values["start_segment"].set(str(words[10]))
- values["default_speed"].set(str(words[11] | (words[12] << 16)))
- values["start_speed"].set(str(words[13] | (words[14] << 16)))
- values["stop_speed"].set(str(words[16] | (words[17] << 16)))
- values["acceleration"].set(str(words[18]))
- values["deceleration"].set(str(words[19]))
-
- def _set_segment_words(self, segments):
- if len(segments) != SEGMENT_COUNT:
- raise ValueError("段参数回读数量错误")
- for index, words in enumerate(segments):
- if len(words) != SEGMENT_WORDS:
- raise ValueError("第 %d 段回读长度错误" % (index + 1))
- variables = self.segment_vars[index]
- frequency = words[0] | (words[1] << 16)
- pulse_bits = words[2] | (words[3] << 16)
- pulses = pulse_bits - (1 << 32) if pulse_bits & 0x80000000 else pulse_bits
- variables["frequency"].set(str(frequency))
- variables["pulses"].set(str(pulses))
- variables["wait_type"].set(option_label(WAIT_OPTIONS, words[4]))
- variables["wait_ms"].set(str(words[5]))
- variables["act_ms"].set(str(words[6]))
- variables["jump"].set(str(words[7]))
-
- def _read_all(self):
- if self._start_operation("正在读取公共参数和 10 段参数"):
- self.worker.submit("read_configuration", operation="读取参数")
-
- def _write_common(self):
- try:
- words = self._common_words()
- except ValueError as error:
- messagebox.showerror("参数错误", str(error))
- return
- if self._start_operation("正在写入公共参数"):
- self.worker.submit(
- "write_common", words=words, operation="写入公共参数"
- )
-
- def _write_segments(self):
- try:
- segments = self._segment_words()
- except ValueError as error:
- messagebox.showerror("参数错误", str(error))
- return
- if self._start_operation("正在写入 10 段参数"):
- self.worker.submit(
- "write_segments", segments=segments, operation="写入段参数"
- )
-
- def _send_control(self, command):
- messages = {
- COMMAND_START: "启动命令已发送",
- COMMAND_STOP: "停止命令已发送",
- COMMAND_CLEAR: "清零命令已发送",
- }
- if self._start_operation("正在发送控制命令"):
- self.worker.submit(
- "control",
- value=command,
- message=messages[command],
- operation="控制命令",
- )
-
- def _clear_position(self):
- if messagebox.askyesno("确认清零", "确认将累计位置清零?"):
- self._send_control(COMMAND_CLEAR)
-
- def _show_status(self, status):
- self.status_vars["position"].set(str(status.position))
- self.status_vars["frequency"].set("%d Hz" % status.frequency_hz)
- self.status_vars["state"].set(
- "%s (%d)" % (STATUS_NAMES.get(status.state, "未知"), status.state)
- )
- self.status_vars["segment"].set(str(status.segment))
- self.status_vars["error"].set(
- "%s (%d)" % (ERROR_NAMES.get(status.error, "未知"), status.error)
- )
-
- def _handle_result(self, event, payload):
- if event == "connection":
- if payload["connected"]:
- description = "%s / 从站 %d" % (payload["port"], payload["slave"])
- self._set_connected(True, description)
- self.footer_var.set("已连接,正在读取参数")
- else:
- reason = payload.get("reason", "")
- self.operation_pending = False
- self.connect_button.configure(state="normal")
- self._set_connected(False, reason)
- self.footer_var.set("连接已断开" if not reason else "通信错误")
- elif event == "configuration":
- self._set_common_words(payload["common"])
- self._set_segment_words(payload["segments"])
- self._finish_operation("全部参数已读取")
- elif event == "common":
- self._set_common_words(payload["words"])
- elif event == "segments":
- self._set_segment_words(payload["words"])
- elif event == "status":
- self._show_status(payload["status"])
- elif event == "operation":
- self._finish_operation(payload["message"])
- elif event == "error":
- message = "%s失败:%s" % (payload["operation"], payload["message"])
- self._finish_operation(message)
- self.footer_label.configure(style="Error.TLabel")
- if payload.get("modal", False):
- messagebox.showerror("PLSR 通信错误", message)
-
- def _drain_results(self):
- try:
- while True:
- event, payload = self.worker.results.get_nowait()
- try:
- self._handle_result(event, payload)
- except (KeyError, ValueError) as error:
- self._finish_operation("界面数据错误:%s" % error)
- except queue.Empty:
- pass
- self.root.after(50, self._drain_results)
-
- def _on_close(self):
- self.worker.close()
- self.root.destroy()
-
-
- class _SelfTestVariable:
- def __init__(self, value):
- self.value = str(value)
-
- def get(self):
- return self.value
-
- def set(self, value):
- self.value = str(value)
-
-
- def self_test():
- assert BAUD_RATE == 9600
- assert INPUT_OPTIONS == (("X4 (0)", 0), ("X5 (1)", 1))
- assert [SEGMENT_1_BASE + index * SEGMENT_STRIDE
- for index in range(SEGMENT_COUNT)] == [
- 0x1100, 0x1110, 0x1120, 0x1130, 0x1140,
- 0x1150, 0x1160, 0x1170, 0x1180, 0x1190,
- ]
- assert parse_integer("0x10", "测试值", 0, 16) == 16
- try:
- parse_integer("17", "测试值", 0, 16)
- except ValueError:
- pass
- else:
- raise AssertionError("range validation did not reject 17")
-
- panel = PlsrControlPanel.__new__(PlsrControlPanel)
- panel.common_vars = {
- "pulse_output": _SelfTestVariable(PULSE_OPTIONS[3][0]),
- "direction_output": _SelfTestVariable(DIRECTION_OPTIONS[3][0]),
- "wait_input": _SelfTestVariable(INPUT_OPTIONS[1][0]),
- "ext_input": _SelfTestVariable(INPUT_OPTIONS[0][0]),
- "send_mode": _SelfTestVariable(SEND_OPTIONS[1][0]),
- "direction_delay": _SelfTestVariable(25),
- "negative_logic": _SelfTestVariable(LOGIC_OPTIONS[1][0]),
- "curve_mode": _SelfTestVariable(CURVE_OPTIONS[2][0]),
- "position_mode": _SelfTestVariable(POSITION_OPTIONS[1][0]),
- "segment_count": _SelfTestVariable(10),
- "start_segment": _SelfTestVariable(2),
- "default_speed": _SelfTestVariable(100000),
- "start_speed": _SelfTestVariable(0),
- "stop_speed": _SelfTestVariable(65536),
- "acceleration": _SelfTestVariable(1000),
- "deceleration": _SelfTestVariable(2000),
- }
- common = panel._common_words()
- assert common[:11] == [3, 3, 1, 0, 1, 25, 1, 2, 1, 10, 2]
- assert common[11:20] == [34464, 1, 0, 0, 0, 0, 1, 1000, 2000]
-
- panel.segment_vars = []
- for index in range(SEGMENT_COUNT):
- panel.segment_vars.append({
- "frequency": _SelfTestVariable(1000 + index),
- "pulses": _SelfTestVariable(-1 if index == 0 else index),
- "wait_type": _SelfTestVariable(WAIT_OPTIONS[4][0]),
- "wait_ms": _SelfTestVariable(index),
- "act_ms": _SelfTestVariable(index + 1),
- "jump": _SelfTestVariable(0),
- })
- segments = panel._segment_words()
- assert len(segments) == SEGMENT_COUNT
- assert all(len(words) == SEGMENT_WORDS for words in segments)
- assert segments[0] == [1000, 0, 0xFFFF, 0xFFFF, 4, 0, 1, 0]
-
- panel.common_vars["segment_count"].set(1)
- panel.segment_vars[0]["jump"].set(2)
- try:
- panel._segment_words()
- except ValueError:
- pass
- else:
- raise AssertionError("active segment jump exceeded segment count")
- print("PLSR control panel self-test passed; no GUI or serial port was opened")
-
-
- def parse_arguments(arguments=None):
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- "--self-test",
- action="store_true",
- help="validate register encoding without opening the GUI or serial port",
- )
- return parser.parse_args(arguments)
-
-
- def main(arguments=None):
- args = parse_arguments(arguments)
- if args.self_test:
- self_test()
- return 0
- root = tk.Tk()
- PlsrControlPanel(root)
- root.mainloop()
- return 0
-
-
- if __name__ == "__main__":
- raise SystemExit(main())
|