优化激光示踪配置工具界面
This commit is contained in:
@@ -0,0 +1,385 @@
|
|||||||
|
"""
|
||||||
|
激光示踪设备网关配置工具
|
||||||
|
通过串口(RS485)直接修改和查看网关参数
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class LaserTracingConfigApp:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("激光示踪配置工具")
|
||||||
|
self.root.geometry("960x720")
|
||||||
|
self.root.resizable(True, True)
|
||||||
|
|
||||||
|
self.serial_port = None
|
||||||
|
self.serial_thread = None
|
||||||
|
self.running = False
|
||||||
|
self.recv_buffer = ""
|
||||||
|
self._port_details = {}
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._refresh_ports()
|
||||||
|
|
||||||
|
# ─────────────────── UI构建 ───────────────────
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
# 串口连接区域
|
||||||
|
conn_frame = ttk.LabelFrame(self.root, text="串口连接", padding=8)
|
||||||
|
conn_frame.pack(fill=tk.X, padx=8, pady=(8, 4))
|
||||||
|
|
||||||
|
# ── 串口: 选择 + 波特率 + 数据位 + 停止位 + 校验 + 打开按钮 ──
|
||||||
|
ttk.Label(conn_frame, text="串口:").grid(row=0, column=0, sticky=tk.W, padx=2)
|
||||||
|
self.port_combo = ttk.Combobox(conn_frame, width=35, state="readonly")
|
||||||
|
self.port_combo.grid(row=0, column=1, padx=2)
|
||||||
|
ttk.Button(conn_frame, text="刷新", width=5, command=self._refresh_ports).grid(row=0, column=2, padx=2)
|
||||||
|
|
||||||
|
ttk.Label(conn_frame, text="波特率:").grid(row=0, column=3, sticky=tk.W, padx=(12, 2))
|
||||||
|
self.baudrate_var = tk.StringVar(value="9600")
|
||||||
|
ttk.Label(conn_frame, textvariable=self.baudrate_var, width=8).grid(row=0, column=4, padx=2)
|
||||||
|
|
||||||
|
ttk.Label(conn_frame, text="数据位:").grid(row=0, column=5, sticky=tk.W, padx=(12, 2))
|
||||||
|
self.databits_var = tk.StringVar(value="8")
|
||||||
|
ttk.Label(conn_frame, textvariable=self.databits_var, width=4).grid(row=0, column=6, padx=2)
|
||||||
|
|
||||||
|
ttk.Label(conn_frame, text="停止位:").grid(row=0, column=7, sticky=tk.W, padx=(12, 2))
|
||||||
|
self.stopbits_var = tk.StringVar(value="1")
|
||||||
|
ttk.Label(conn_frame, textvariable=self.stopbits_var, width=4).grid(row=0, column=8, padx=2)
|
||||||
|
|
||||||
|
ttk.Label(conn_frame, text="校验:").grid(row=0, column=9, sticky=tk.W, padx=(12, 2))
|
||||||
|
self.parity_var = tk.StringVar(value="None")
|
||||||
|
ttk.Label(conn_frame, textvariable=self.parity_var, width=6).grid(row=0, column=10, padx=2)
|
||||||
|
|
||||||
|
self.connect_btn = ttk.Button(conn_frame, text="打开串口", width=10, command=self._toggle_connection)
|
||||||
|
self.connect_btn.grid(row=0, column=11, padx=(12, 2))
|
||||||
|
|
||||||
|
# 参数配置区域
|
||||||
|
config_frame = ttk.LabelFrame(self.root, text="参数配置", padding=8)
|
||||||
|
config_frame.pack(fill=tk.X, padx=8, pady=4)
|
||||||
|
|
||||||
|
# Lora参数
|
||||||
|
lora_frame = ttk.LabelFrame(config_frame, text="Lora 参数", padding=6)
|
||||||
|
lora_frame.pack(fill=tk.X, pady=(0, 4))
|
||||||
|
|
||||||
|
ttk.Label(lora_frame, text="信道频率(fc):").grid(row=0, column=0, sticky=tk.W, padx=2)
|
||||||
|
self._fc_display_to_value = {}
|
||||||
|
self._fc_value_to_display = {}
|
||||||
|
fc_displays = []
|
||||||
|
freq = 410100000
|
||||||
|
ch = 1
|
||||||
|
while freq < 525000000:
|
||||||
|
display = f"信道{ch}: {freq / 1000000:.1f}MHz"
|
||||||
|
fc_displays.append(display)
|
||||||
|
self._fc_display_to_value[display] = str(freq)
|
||||||
|
self._fc_value_to_display[str(freq)] = display
|
||||||
|
freq += 200000
|
||||||
|
ch += 1
|
||||||
|
self.lora_fc_display_var = tk.StringVar(value=self._fc_value_to_display.get("433100000", fc_displays[0] if fc_displays else ""))
|
||||||
|
self.lora_fc_combo = ttk.Combobox(lora_frame, textvariable=self.lora_fc_display_var,
|
||||||
|
values=fc_displays, width=20, state="readonly")
|
||||||
|
self.lora_fc_combo.grid(row=0, column=1, padx=2)
|
||||||
|
ttk.Button(lora_frame, text="发送", command=self._send_lora_config).grid(row=0, column=3, padx=8)
|
||||||
|
|
||||||
|
# 查询按钮行
|
||||||
|
query_frame = ttk.Frame(config_frame)
|
||||||
|
query_frame.pack(fill=tk.X, pady=4)
|
||||||
|
ttk.Button(query_frame, text="查询当前配置 (para)", command=self._send_para).pack(side=tk.LEFT, padx=4)
|
||||||
|
ttk.Label(query_frame, text="配置后可通过para命令验证是否成功",
|
||||||
|
foreground="gray").pack(side=tk.LEFT, padx=8)
|
||||||
|
|
||||||
|
# 参数显示区域
|
||||||
|
para_frame = ttk.LabelFrame(self.root, text="设备参数信息", padding=8)
|
||||||
|
para_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
|
||||||
|
|
||||||
|
# 使用Treeview展示解析后的参数(双列无表头,不可选定)
|
||||||
|
columns = ("参数", "值")
|
||||||
|
self.para_tree = ttk.Treeview(para_frame, columns=columns, show="tree headings",
|
||||||
|
height=10, selectmode="none")
|
||||||
|
self.para_tree.heading("#0", text="")
|
||||||
|
self.para_tree.heading("参数", text="")
|
||||||
|
self.para_tree.heading("值", text="")
|
||||||
|
self.para_tree.column("#0", width=0, stretch=False)
|
||||||
|
self.para_tree.column("参数", width=180, anchor=tk.W)
|
||||||
|
self.para_tree.column("值", width=380, anchor=tk.W)
|
||||||
|
tree_scroll = ttk.Scrollbar(para_frame, orient=tk.VERTICAL, command=self.para_tree.yview)
|
||||||
|
self.para_tree.configure(yscrollcommand=tree_scroll.set)
|
||||||
|
self.para_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
# 预填充参数表格(值为"-"待读取后更新)
|
||||||
|
self._para_order = ["软件版本", "硬件版本", "本机MAC", "电池电量", "设备类型",
|
||||||
|
"通信方式", "设备标识", "网关MAC", "注册状态",
|
||||||
|
"RS485_1", "RS485_2", "Lora参数", "低功耗配置",
|
||||||
|
"设备时间", "错误信息"]
|
||||||
|
self._para_value_items = {}
|
||||||
|
for key in self._para_order:
|
||||||
|
item_id = self.para_tree.insert("", tk.END, values=(key, "-"))
|
||||||
|
self._para_value_items[key] = item_id
|
||||||
|
|
||||||
|
# 通讯日志区域
|
||||||
|
log_frame = ttk.LabelFrame(self.root, text="通讯日志", padding=4)
|
||||||
|
log_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=(4, 8))
|
||||||
|
|
||||||
|
log_inner = ttk.Frame(log_frame)
|
||||||
|
log_inner.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 左侧:日志文本 + 底部清空按钮
|
||||||
|
log_left = ttk.Frame(log_inner)
|
||||||
|
log_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.log_text = scrolledtext.ScrolledText(log_left, height=10, font=("Consolas", 9),
|
||||||
|
state=tk.DISABLED, wrap=tk.WORD,
|
||||||
|
bg="#1E1E1E", fg="#00FF00",
|
||||||
|
insertbackground="#00FF00")
|
||||||
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
ttk.Button(log_left, text="清空日志", command=self._clear_log).pack(anchor=tk.W, pady=(2, 0))
|
||||||
|
|
||||||
|
# 右侧:自定义指令
|
||||||
|
cmd_frame = ttk.LabelFrame(log_inner, text="自定义指令", padding=6)
|
||||||
|
cmd_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(6, 0))
|
||||||
|
|
||||||
|
self.custom_cmd_var = tk.StringVar()
|
||||||
|
cmd_entry = ttk.Entry(cmd_frame, textvariable=self.custom_cmd_var, width=20)
|
||||||
|
cmd_entry.pack(pady=(0, 4))
|
||||||
|
cmd_entry.bind("<Return>", lambda e: self._send_custom_cmd())
|
||||||
|
ttk.Button(cmd_frame, text="发送", command=self._send_custom_cmd).pack()
|
||||||
|
|
||||||
|
# 日志颜色标签
|
||||||
|
self.log_text.tag_configure("send", foreground="#00CCFF")
|
||||||
|
self.log_text.tag_configure("recv", foreground="#00FF00")
|
||||||
|
self.log_text.tag_configure("error", foreground="#FF4444")
|
||||||
|
self.log_text.tag_configure("info", foreground="#88FF88")
|
||||||
|
|
||||||
|
# ─────────────────── 串口操作 ───────────────────
|
||||||
|
|
||||||
|
def _refresh_ports(self):
|
||||||
|
port_list = serial.tools.list_ports.comports()
|
||||||
|
self._port_details = {}
|
||||||
|
display_names = []
|
||||||
|
for p in sorted(port_list, key=lambda x: x.device):
|
||||||
|
display = p.description
|
||||||
|
# 如果多个端口有相同描述,追加设备名区分
|
||||||
|
if display in self._port_details:
|
||||||
|
display = f"{display} ({p.device})"
|
||||||
|
display_names.append(display)
|
||||||
|
self._port_details[display] = p.device
|
||||||
|
self.port_combo["values"] = display_names
|
||||||
|
if display_names:
|
||||||
|
self.port_combo.set(display_names[0])
|
||||||
|
|
||||||
|
def _toggle_connection(self):
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
self._disconnect()
|
||||||
|
else:
|
||||||
|
self._connect()
|
||||||
|
|
||||||
|
def _connect(self):
|
||||||
|
selected = self.port_combo.get()
|
||||||
|
port = self._port_details.get(selected)
|
||||||
|
if not port:
|
||||||
|
messagebox.showwarning("警告", "请选择串口")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serial_port = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=int(self.baudrate_var.get()),
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
timeout=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.recv_buffer = ""
|
||||||
|
self.serial_thread = threading.Thread(target=self._recv_loop, daemon=True)
|
||||||
|
self.serial_thread.start()
|
||||||
|
|
||||||
|
self.connect_btn.configure(text="关闭串口")
|
||||||
|
self.port_combo.configure(state=tk.DISABLED)
|
||||||
|
self._log(f"已连接 {port} @ {self.baudrate_var.get()}", "info")
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
messagebox.showerror("连接失败", str(e))
|
||||||
|
self._log(f"连接失败: {e}", "error")
|
||||||
|
|
||||||
|
def _disconnect(self):
|
||||||
|
self.running = False
|
||||||
|
if self.serial_thread:
|
||||||
|
self.serial_thread.join(timeout=1)
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
self.serial_port.close()
|
||||||
|
self.serial_port = None
|
||||||
|
self.connect_btn.configure(text="打开串口")
|
||||||
|
self.port_combo.configure(state="readonly")
|
||||||
|
self._log("已断开连接", "info")
|
||||||
|
|
||||||
|
def _recv_loop(self):
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
n = self.serial_port.in_waiting
|
||||||
|
if n > 0:
|
||||||
|
data = self.serial_port.read(n)
|
||||||
|
text = data.decode("utf-8", errors="replace")
|
||||||
|
self.recv_buffer += text
|
||||||
|
self.root.after(0, self._on_data_received, text)
|
||||||
|
else:
|
||||||
|
time.sleep(0.05)
|
||||||
|
else:
|
||||||
|
time.sleep(0.1)
|
||||||
|
except Exception as e:
|
||||||
|
if self.running:
|
||||||
|
self.root.after(0, self._log, f"接收错误: {e}", "error")
|
||||||
|
break
|
||||||
|
|
||||||
|
def _on_data_received(self, text):
|
||||||
|
self._log(text, "recv", newline=False)
|
||||||
|
# 检查是否收到完整para回应(以***结尾)
|
||||||
|
if "**********************************************************" in self.recv_buffer:
|
||||||
|
self._parse_para_response(self.recv_buffer)
|
||||||
|
self.recv_buffer = ""
|
||||||
|
|
||||||
|
def _send_command(self, cmd):
|
||||||
|
if not self.serial_port or not self.serial_port.is_open:
|
||||||
|
messagebox.showwarning("警告", "请先连接串口")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.recv_buffer = ""
|
||||||
|
self.serial_port.write((cmd + "\r\n").encode("utf-8"))
|
||||||
|
self._log(f">>> {cmd}", "send")
|
||||||
|
except serial.SerialException as e:
|
||||||
|
self._log(f"发送失败: {e}", "error")
|
||||||
|
|
||||||
|
def _send_custom_cmd(self):
|
||||||
|
cmd = self.custom_cmd_var.get().strip()
|
||||||
|
if cmd:
|
||||||
|
self._send_command(cmd)
|
||||||
|
self.custom_cmd_var.set("")
|
||||||
|
|
||||||
|
# ─────────────────── 指令发送 ───────────────────
|
||||||
|
|
||||||
|
def _send_para(self):
|
||||||
|
self._send_command("para")
|
||||||
|
|
||||||
|
def _send_lora_config(self):
|
||||||
|
display = self.lora_fc_display_var.get().strip()
|
||||||
|
fc = self._fc_display_to_value.get(display)
|
||||||
|
if not fc:
|
||||||
|
messagebox.showwarning("参数错误", "请选择信道频率")
|
||||||
|
return
|
||||||
|
self._send_command(f"lora fc {fc}")
|
||||||
|
|
||||||
|
# ─────────────────── 回应解析 ───────────────────
|
||||||
|
|
||||||
|
def _parse_para_response(self, text):
|
||||||
|
# 清空Treeview
|
||||||
|
for item in self.para_tree.get_children():
|
||||||
|
self.para_tree.delete(item)
|
||||||
|
|
||||||
|
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||||
|
|
||||||
|
key_value_patterns = [
|
||||||
|
(r"SoftWare V:(.*)", "软件版本"),
|
||||||
|
(r"HardWare V:(.*)", "硬件版本"),
|
||||||
|
(r"Local MAC:\s*(.*)", "本机MAC"),
|
||||||
|
(r"Battery:\s*(.*)", "电池电量"),
|
||||||
|
(r"LocalType:\s*(.*)", "设备类型"),
|
||||||
|
(r"CommInf:\s*(.*)", "通信方式"),
|
||||||
|
(r"DeviceSign:\s*(.*)", "设备标识"),
|
||||||
|
(r"GateWay MAC:\s*(.*)", "网关MAC"),
|
||||||
|
(r"Register Status:\s*(.*)", "注册状态"),
|
||||||
|
(r"RS485h1:\s*(.*)", "RS485_1"),
|
||||||
|
(r"RS485h2:\s*(.*)", "RS485_2"),
|
||||||
|
(r"Lora:\s*(.*)", "Lora参数"),
|
||||||
|
(r"LpCfg:\s*(.*)", "低功耗配置"),
|
||||||
|
]
|
||||||
|
|
||||||
|
found = {}
|
||||||
|
for line in lines:
|
||||||
|
for pattern, label in key_value_patterns:
|
||||||
|
m = re.search(pattern, line)
|
||||||
|
if m:
|
||||||
|
found[label] = m.group(1).strip()
|
||||||
|
|
||||||
|
# 解析日期时间
|
||||||
|
dt_match = re.search(r"(\d{4}/\d{1,2}/\d{1,2}-\d{1,2}:\d{2}:\d{2})", text)
|
||||||
|
if dt_match:
|
||||||
|
found["设备时间"] = dt_match.group(1)
|
||||||
|
|
||||||
|
# 检查Error
|
||||||
|
err_match = re.search(r"Error:\s*(.*)", text)
|
||||||
|
if err_match:
|
||||||
|
found["错误信息"] = err_match.group(1).strip()
|
||||||
|
|
||||||
|
# 重建参数表格(每次收到完整响应时)
|
||||||
|
for item in self.para_tree.get_children():
|
||||||
|
self.para_tree.delete(item)
|
||||||
|
self._para_value_items = {}
|
||||||
|
for key in self._para_order:
|
||||||
|
value = found.get(key, "-")
|
||||||
|
item_id = self.para_tree.insert("", tk.END, values=(key, value))
|
||||||
|
self._para_value_items[key] = item_id
|
||||||
|
|
||||||
|
# 解析Lora详细参数并更新配置界面
|
||||||
|
if "Lora参数" in found:
|
||||||
|
lora_str = found["Lora参数"]
|
||||||
|
fc_match = re.search(r"fc\s+(\d+)", lora_str)
|
||||||
|
if fc_match:
|
||||||
|
fc_val = fc_match.group(1)
|
||||||
|
display = self._fc_value_to_display.get(fc_val)
|
||||||
|
if display:
|
||||||
|
self.lora_fc_display_var.set(display)
|
||||||
|
else:
|
||||||
|
new_display = f"自定义: {int(fc_val) / 1000000:.1f}MHz"
|
||||||
|
self._fc_display_to_value[new_display] = fc_val
|
||||||
|
self._fc_value_to_display[fc_val] = new_display
|
||||||
|
self.lora_fc_combo["values"] = list(self.lora_fc_combo["values"]) + [new_display]
|
||||||
|
self.lora_fc_display_var.set(new_display)
|
||||||
|
|
||||||
|
# 解析RS485波特率并提示
|
||||||
|
if "RS485_1" in found:
|
||||||
|
baud_match = re.search(r"Baudrate\s+(\d+)", found["RS485_1"])
|
||||||
|
if baud_match:
|
||||||
|
self.baudrate_var.set(baud_match.group(1))
|
||||||
|
|
||||||
|
# ─────────────────── 日志 ───────────────────
|
||||||
|
|
||||||
|
def _clear_log(self):
|
||||||
|
self.log_text.configure(state=tk.NORMAL)
|
||||||
|
self.log_text.delete("1.0", tk.END)
|
||||||
|
self.log_text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def _log(self, message, tag="info", newline=True):
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
self.log_text.configure(state=tk.NORMAL)
|
||||||
|
if newline:
|
||||||
|
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag)
|
||||||
|
else:
|
||||||
|
self.log_text.insert(tk.END, f"[{timestamp}] {message}", tag)
|
||||||
|
self.log_text.see(tk.END)
|
||||||
|
self.log_text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = tk.Tk()
|
||||||
|
app = LaserTracingConfigApp(root)
|
||||||
|
|
||||||
|
def on_closing():
|
||||||
|
app._disconnect()
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
root.protocol("WM_DELETE_WINDOW", on_closing)
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user