|
|
|
@@ -0,0 +1,935 @@ |
|
|
|
"""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()) |