478 lines
20 KiB
Python
478 lines
20 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, messagebox, simpledialog
|
|
import cv2
|
|
from config import config_manager
|
|
import threading
|
|
import serial.tools.list_ports
|
|
|
|
class ConfigGUI:
|
|
"""配置GUI界面"""
|
|
def __init__(self):
|
|
self.root = tk.Tk()
|
|
self.root.title("配置管理 - 火炬之光自动化")
|
|
self.root.geometry("1000x720")
|
|
self.root.minsize(900, 600) # 设置最小尺寸
|
|
|
|
self.selected_index = 0
|
|
self.setup_ui()
|
|
self.load_current_config()
|
|
|
|
def setup_ui(self):
|
|
"""设置UI界面"""
|
|
# 左侧:配置组列表
|
|
left_frame = ttk.Frame(self.root, padding="10")
|
|
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False)
|
|
|
|
ttk.Label(left_frame, text="配置组列表", font=('Arial', 12, 'bold')).pack()
|
|
|
|
# 配置组列表
|
|
self.group_listbox = tk.Listbox(left_frame, width=20, height=25)
|
|
self.group_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
self.group_listbox.bind('<<ListboxSelect>>', self.on_group_select)
|
|
|
|
# 按钮
|
|
btn_frame = ttk.Frame(left_frame)
|
|
btn_frame.pack(fill=tk.X)
|
|
|
|
ttk.Button(btn_frame, text="添加组", command=self.add_group).pack(fill=tk.X, pady=2)
|
|
ttk.Button(btn_frame, text="删除组", command=self.delete_group).pack(fill=tk.X, pady=2)
|
|
ttk.Button(btn_frame, text="设为活动", command=self.set_active).pack(fill=tk.X, pady=2)
|
|
|
|
# 右侧:配置详情
|
|
right_frame = ttk.Frame(self.root, padding="10")
|
|
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
|
|
ttk.Label(right_frame, text="配置详情", font=('Arial', 12, 'bold')).pack()
|
|
|
|
# 配置项
|
|
self.config_vars = {}
|
|
|
|
# 基本配置
|
|
basic_frame = ttk.LabelFrame(right_frame, text="基本配置", padding="10")
|
|
basic_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.create_entry(basic_frame, "name", "配置组名称:")
|
|
# 采集卡索引:使用下拉+扫描
|
|
cam_row = ttk.Frame(basic_frame)
|
|
cam_row.pack(fill=tk.X, pady=3)
|
|
ttk.Label(cam_row, text="采集卡索引:", width=15).pack(side=tk.LEFT)
|
|
self.camera_index_var = tk.StringVar()
|
|
self.camera_index_cb = ttk.Combobox(cam_row, textvariable=self.camera_index_var, width=23, state="readonly")
|
|
self.camera_index_cb.pack(side=tk.LEFT, padx=5)
|
|
ttk.Button(cam_row, text="扫描采集卡", command=self.scan_cameras).pack(side=tk.LEFT, padx=5)
|
|
# 将变量放进config_vars统一管理
|
|
self.config_vars["camera_index"] = self.camera_index_var
|
|
self.create_entry(basic_frame, "camera_width", "采集宽度:")
|
|
self.create_entry(basic_frame, "camera_height", "采集高度:")
|
|
|
|
# 串口配置
|
|
serial_frame = ttk.LabelFrame(right_frame, text="串口配置", padding="10")
|
|
serial_frame.pack(fill=tk.X, pady=5)
|
|
|
|
# 串口:使用下拉+扫描
|
|
port_row = ttk.Frame(serial_frame)
|
|
port_row.pack(fill=tk.X, pady=3)
|
|
ttk.Label(port_row, text="串口:", width=15).pack(side=tk.LEFT)
|
|
self.serial_port_var = tk.StringVar()
|
|
self.serial_port_cb = ttk.Combobox(port_row, textvariable=self.serial_port_var, width=23, state="readonly")
|
|
self.serial_port_cb.pack(side=tk.LEFT, padx=5)
|
|
ttk.Button(port_row, text="扫描串口", command=self.scan_ports).pack(side=tk.LEFT, padx=5)
|
|
# 将变量放进config_vars统一管理
|
|
self.config_vars["serial_port"] = self.serial_port_var
|
|
|
|
self.create_entry(serial_frame, "serial_baudrate", "波特率:")
|
|
|
|
# 游戏配置
|
|
game_frame = ttk.LabelFrame(right_frame, text="游戏配置", padding="10")
|
|
game_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.create_entry(game_frame, "move_velocity", "移动速度(v值):")
|
|
|
|
# 预览配置
|
|
preview_frame = ttk.LabelFrame(right_frame, text="预览配置", padding="10")
|
|
preview_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.create_entry(preview_frame, "preview_width", "预览宽度:", prefix="display")
|
|
self.create_entry(preview_frame, "preview_height", "预览高度:", prefix="display")
|
|
self.create_entry(preview_frame, "preview_columns", "预览列数:", prefix="display")
|
|
self.create_entry(preview_frame, "preview_rows", "预览行数:", prefix="display")
|
|
|
|
# 保存按钮
|
|
save_frame = ttk.Frame(right_frame)
|
|
save_frame.pack(fill=tk.X, pady=10)
|
|
|
|
ttk.Button(save_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=5)
|
|
ttk.Button(save_frame, text="启动预览", command=self.start_preview).pack(side=tk.LEFT, padx=5)
|
|
|
|
# 启动程序按钮组
|
|
start_frame = ttk.Frame(right_frame)
|
|
start_frame.pack(fill=tk.X, pady=5)
|
|
|
|
ttk.Button(start_frame, text="启动单个配置组", command=self.start_program).pack(side=tk.LEFT, padx=5)
|
|
ttk.Button(start_frame, text="启动多个配置组", command=self.start_multi_program).pack(side=tk.LEFT, padx=5)
|
|
|
|
def create_entry(self, parent, key, label, prefix=None):
|
|
"""创建输入框"""
|
|
frame = ttk.Frame(parent)
|
|
frame.pack(fill=tk.X, pady=3)
|
|
|
|
ttk.Label(frame, text=label, width=15).pack(side=tk.LEFT)
|
|
|
|
var = tk.StringVar()
|
|
entry = ttk.Entry(frame, textvariable=var, width=25)
|
|
entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
|
|
|
# 保存变量引用
|
|
if prefix:
|
|
full_key = f"{prefix}_{key}"
|
|
else:
|
|
full_key = key
|
|
self.config_vars[full_key] = var
|
|
|
|
def load_current_config(self):
|
|
"""加载当前配置到界面"""
|
|
# 加载配置组列表
|
|
self.update_group_list()
|
|
|
|
# 如果有配置组,默认选择第一个
|
|
if len(config_manager.config['groups']) > 0:
|
|
self.selected_index = 0
|
|
self.group_listbox.selection_set(0)
|
|
self.load_group_config(0)
|
|
|
|
# 加载预览配置
|
|
display = config_manager.config.get('display', {})
|
|
for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']:
|
|
if key in display:
|
|
self.config_vars[f"display_{key}"].set(str(display[key]))
|
|
# 初次自动扫描一次采集卡和串口
|
|
try:
|
|
self.scan_cameras()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.scan_ports()
|
|
except Exception:
|
|
pass
|
|
|
|
def update_group_list(self):
|
|
"""更新配置组列表"""
|
|
self.group_listbox.delete(0, tk.END)
|
|
for i, group in enumerate(config_manager.config['groups']):
|
|
status = "✓" if group.get('active', False) else " "
|
|
self.group_listbox.insert(tk.END, f"{status} {group['name']}")
|
|
|
|
def on_group_select(self, event):
|
|
"""选择配置组"""
|
|
selection = self.group_listbox.curselection()
|
|
if selection:
|
|
self.selected_index = selection[0]
|
|
self.load_group_config(self.selected_index)
|
|
|
|
def load_group_config(self, index):
|
|
"""加载配置组详情"""
|
|
group = config_manager.get_group_by_index(index)
|
|
if group:
|
|
for key, var in self.config_vars.items():
|
|
if not key.startswith('display_'):
|
|
if key in group:
|
|
var.set(str(group[key]))
|
|
# 确保下拉框列表与当前值一致
|
|
if group and 'camera_index' in group:
|
|
# 如果当前索引不在选项里,追加
|
|
values = list(self.camera_index_cb.cget('values')) if self.camera_index_cb.cget('values') else []
|
|
display_value = f"{group['camera_index']}"
|
|
if display_value not in values:
|
|
values.append(display_value)
|
|
self.camera_index_cb['values'] = values
|
|
self.camera_index_cb.set(display_value)
|
|
|
|
# 确保串口下拉框列表与当前值一致
|
|
if group and 'serial_port' in group:
|
|
# 如果当前串口不在选项里,追加
|
|
values = list(self.serial_port_cb.cget('values')) if self.serial_port_cb.cget('values') else []
|
|
port_value = group['serial_port']
|
|
if port_value not in values:
|
|
values.append(port_value)
|
|
self.serial_port_cb['values'] = values
|
|
self.serial_port_cb.set(port_value)
|
|
|
|
def scan_cameras(self, max_index: int = 10):
|
|
"""扫描系统可用的采集卡索引,并填充下拉框"""
|
|
import warnings
|
|
found = []
|
|
|
|
# 临时设置OpenCV日志级别
|
|
import os
|
|
old_level = os.environ.get('OPENCV_LOG_LEVEL', '')
|
|
os.environ['OPENCV_LOG_LEVEL'] = 'ERROR'
|
|
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
|
|
|
|
try:
|
|
for idx in range(max_index + 1):
|
|
cap = None
|
|
try:
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings('ignore')
|
|
# 尝试DSHOW后端
|
|
cap = cv2.VideoCapture(idx, cv2.CAP_DSHOW)
|
|
if not cap.isOpened():
|
|
# 回退默认后端再试
|
|
cap = cv2.VideoCapture(idx)
|
|
|
|
if cap.isOpened():
|
|
# 测试读取一帧,确保真正可用
|
|
ret, test_frame = cap.read()
|
|
if ret and test_frame is not None:
|
|
found.append(str(idx))
|
|
cap.release()
|
|
except Exception:
|
|
if cap:
|
|
try:
|
|
cap.release()
|
|
except:
|
|
pass
|
|
continue
|
|
finally:
|
|
# 恢复原来的日志级别
|
|
if old_level:
|
|
os.environ['OPENCV_LOG_LEVEL'] = old_level
|
|
else:
|
|
os.environ.pop('OPENCV_LOG_LEVEL', None)
|
|
|
|
if not found:
|
|
found = ["0"] # 至少给一个默认项,避免为空
|
|
messagebox.showwarning("扫描完成", "未发现可用采集卡,已添加默认选项 0")
|
|
else:
|
|
messagebox.showinfo("扫描完成", f"发现可用采集卡索引: {', '.join(found)}")
|
|
|
|
self.camera_index_cb['values'] = found
|
|
# 若当前无选择,则选择第一项
|
|
if not self.camera_index_var.get() and found:
|
|
self.camera_index_cb.set(found[0])
|
|
|
|
def scan_ports(self):
|
|
"""扫描系统可用的串口,并填充下拉框"""
|
|
found_real = []
|
|
try:
|
|
ports = serial.tools.list_ports.comports()
|
|
found_real = [port.device for port in ports]
|
|
# 按端口名排序
|
|
found_real.sort(key=lambda x: int(x.replace('COM', '')) if x.replace('COM', '').isdigit() else 999)
|
|
except Exception as e:
|
|
print(f"扫描串口错误: {e}")
|
|
|
|
# 如果没有发现实际端口,使用默认端口列表
|
|
if not found_real:
|
|
found = ["COM1", "COM2", "COM3", "COM4", "COM5", "COM6"] # 默认给一些常见端口
|
|
messagebox.showwarning("警告", "未发现可用串口设备,已添加常用默认选项")
|
|
else:
|
|
found = found_real
|
|
messagebox.showinfo("扫描完成", f"发现可用串口: {', '.join(found)}")
|
|
|
|
self.serial_port_cb['values'] = found
|
|
# 若当前无选择,则选择第一项
|
|
if not self.serial_port_var.get() and found:
|
|
self.serial_port_cb.set(found[0])
|
|
|
|
def add_group(self):
|
|
"""添加配置组"""
|
|
name = simpledialog.askstring("添加配置组", "请输入配置组名称:", initialvalue=f"配置组{len(config_manager.config['groups'])+1}")
|
|
if name:
|
|
config_manager.add_group(name)
|
|
self.update_group_list()
|
|
messagebox.showinfo("成功", f"已添加配置组: {name}")
|
|
|
|
def delete_group(self):
|
|
"""删除配置组"""
|
|
selection = self.group_listbox.curselection()
|
|
if not selection:
|
|
messagebox.showwarning("警告", "请先选择要删除的配置组")
|
|
return
|
|
|
|
if messagebox.askyesno("确认", "确定要删除选中的配置组吗?"):
|
|
config_manager.delete_group(selection[0])
|
|
self.update_group_list()
|
|
messagebox.showinfo("成功", "配置组已删除")
|
|
|
|
def set_active(self):
|
|
"""设置为活动配置组"""
|
|
selection = self.group_listbox.curselection()
|
|
if not selection:
|
|
messagebox.showwarning("警告", "请先选择要设为活动的配置组")
|
|
return
|
|
|
|
config_manager.set_active_group(selection[0])
|
|
self.update_group_list()
|
|
messagebox.showinfo("成功", "已设置为活动配置组")
|
|
|
|
def save_config(self):
|
|
"""保存当前编辑的配置"""
|
|
# 检查索引有效性
|
|
if self.selected_index >= len(config_manager.config['groups']):
|
|
messagebox.showerror("错误", f"配置组索引 {self.selected_index} 无效")
|
|
return False
|
|
|
|
# 保存当前组的配置
|
|
group = config_manager.get_group_by_index(self.selected_index)
|
|
if group:
|
|
for key, var in self.config_vars.items():
|
|
if not key.startswith('display_'):
|
|
try:
|
|
value_str = var.get().strip()
|
|
# 特殊处理:某些字段需要转换为整数
|
|
if key in ['camera_index', 'camera_width', 'camera_height', 'serial_baudrate', 'move_velocity']:
|
|
try:
|
|
value = int(value_str) if value_str else group.get(key, 0)
|
|
except:
|
|
value = group.get(key, 0)
|
|
else:
|
|
value = value_str if value_str else group.get(key, '')
|
|
group[key] = value
|
|
except Exception as e:
|
|
print(f"保存字段 {key} 时出错: {e}")
|
|
pass
|
|
|
|
# 保存预览配置
|
|
display = config_manager.config.get('display', {})
|
|
for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']:
|
|
var = self.config_vars.get(f"display_{key}")
|
|
if var:
|
|
try:
|
|
display[key] = int(var.get())
|
|
except:
|
|
pass
|
|
config_manager.config['display'] = display
|
|
|
|
# 保存到文件
|
|
if config_manager.save_config():
|
|
# 更新左侧列表显示
|
|
self.update_group_list()
|
|
messagebox.showinfo("成功", "配置已保存")
|
|
return True
|
|
else:
|
|
messagebox.showerror("错误", "配置保存失败")
|
|
return False
|
|
|
|
def start_preview(self):
|
|
"""启动预览窗口"""
|
|
# 保存配置(不显示消息框,静默保存)
|
|
if not self.save_config_silent():
|
|
messagebox.showerror("错误", "配置保存失败,无法启动预览")
|
|
return
|
|
|
|
# 重新加载配置(从文件读取最新配置)
|
|
config_manager.load_config()
|
|
|
|
# 启动预览窗口
|
|
from preview import PreviewWindow
|
|
preview_thread = threading.Thread(target=lambda: PreviewWindow().run())
|
|
preview_thread.daemon = True
|
|
preview_thread.start()
|
|
|
|
def save_config_silent(self):
|
|
"""静默保存配置(不显示消息框)"""
|
|
# 保存当前组的配置
|
|
if self.selected_index >= len(config_manager.config['groups']):
|
|
print(f"⚠️ 选中索引 {self.selected_index} 超出范围")
|
|
return False
|
|
|
|
group = config_manager.get_group_by_index(self.selected_index)
|
|
if group:
|
|
for key, var in self.config_vars.items():
|
|
if not key.startswith('display_'):
|
|
try:
|
|
value_str = var.get().strip()
|
|
# 特殊处理:某些字段需要转换为整数
|
|
if key in ['camera_index', 'camera_width', 'camera_height', 'serial_baudrate', 'move_velocity']:
|
|
try:
|
|
value = int(value_str) if value_str else group.get(key, 0)
|
|
except:
|
|
value = group.get(key, 0)
|
|
else:
|
|
value = value_str if value_str else group.get(key, '')
|
|
group[key] = value
|
|
except Exception as e:
|
|
print(f"保存字段 {key} 时出错: {e}")
|
|
pass
|
|
|
|
# 保存预览配置
|
|
display = config_manager.config.get('display', {})
|
|
for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']:
|
|
var = self.config_vars.get(f"display_{key}")
|
|
if var:
|
|
try:
|
|
display[key] = int(var.get())
|
|
except:
|
|
pass
|
|
config_manager.config['display'] = display
|
|
|
|
# 保存到文件
|
|
result = config_manager.save_config()
|
|
|
|
# 更新左侧列表显示
|
|
if result:
|
|
self.update_group_list()
|
|
|
|
return result
|
|
|
|
def start_program(self):
|
|
"""启动单个配置组的主程序"""
|
|
# 保存配置(静默)
|
|
if not self.save_config_silent():
|
|
messagebox.showerror("错误", "配置保存失败")
|
|
return
|
|
|
|
# 检查是否有活动配置组
|
|
active_groups = [g for g in config_manager.config['groups'] if g.get('active', False)]
|
|
if not active_groups:
|
|
messagebox.showwarning("警告", "没有活动的配置组\n\n请先选择一个配置组并设置为活动")
|
|
return
|
|
|
|
# 启动单个配置组
|
|
import subprocess
|
|
import sys
|
|
try:
|
|
# 找到活动配置组的索引
|
|
active_group = active_groups[0]
|
|
group_index = config_manager.config['groups'].index(active_group)
|
|
|
|
subprocess.Popen([
|
|
sys.executable,
|
|
"main_single.py",
|
|
str(group_index)
|
|
], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0)
|
|
|
|
messagebox.showinfo("成功", f"已启动配置组: {active_group['name']}\n\n请在控制台查看运行状态")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"启动失败: {e}")
|
|
|
|
def start_multi_program(self):
|
|
"""启动多个配置组的主程序"""
|
|
# 保存配置(静默)
|
|
if not self.save_config_silent():
|
|
messagebox.showerror("错误", "配置保存失败")
|
|
return
|
|
|
|
# 启动多配置组管理器
|
|
import subprocess
|
|
import sys
|
|
try:
|
|
subprocess.Popen([
|
|
sys.executable,
|
|
"main_multi.py"
|
|
], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0)
|
|
|
|
messagebox.showinfo("提示", "多配置组启动器已打开\n\n请在控制台中选择要启动的配置组")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"启动失败: {e}")
|
|
|
|
def run(self):
|
|
"""运行GUI"""
|
|
self.root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
app = ConfigGUI()
|
|
app.run()
|
|
|