first commit
This commit is contained in:
533
main.py
Executable file
533
main.py
Executable file
@@ -0,0 +1,533 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DPS-150 Power Supply Management Utility
|
||||
Полнофункциональная утилита для управления блоком питания DPS-150
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
import serial.tools.list_ports
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
import matplotlib.animation as animation
|
||||
from collections import deque
|
||||
import threading
|
||||
import time
|
||||
from dps150 import DPS150
|
||||
from dps150.packets.cmd import (
|
||||
VOLTAGE_SET, CURRENT_SET, OUTPUT_ENABLE, OVP, OCP, OPP, OTP, LVP
|
||||
)
|
||||
|
||||
|
||||
class DPS150GUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("DPS-150 Управление блоком питания")
|
||||
self.root.geometry("1400x900")
|
||||
|
||||
self.device = None
|
||||
self.connected = False
|
||||
self.data = {}
|
||||
|
||||
# Данные для графиков (храним последние 200 точек)
|
||||
self.max_points = 200
|
||||
self.time_data = deque(maxlen=self.max_points)
|
||||
self.voltage_data = deque(maxlen=self.max_points)
|
||||
self.current_data = deque(maxlen=self.max_points)
|
||||
self.power_data = deque(maxlen=self.max_points)
|
||||
self.start_time = time.time()
|
||||
|
||||
self.setup_ui()
|
||||
self.update_serial_ports()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Создание интерфейса"""
|
||||
# Главный контейнер
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
|
||||
# Левая панель - управление
|
||||
left_panel = ttk.Frame(main_frame)
|
||||
left_panel.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
|
||||
|
||||
# Правая панель - графики
|
||||
right_panel = ttk.Frame(main_frame)
|
||||
right_panel.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
main_frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.setup_connection_panel(left_panel)
|
||||
self.setup_info_panel(left_panel)
|
||||
self.setup_control_panel(left_panel)
|
||||
self.setup_protection_panel(left_panel)
|
||||
self.setup_log_panel(left_panel)
|
||||
self.setup_graphs_panel(right_panel)
|
||||
|
||||
def setup_connection_panel(self, parent):
|
||||
"""Панель подключения"""
|
||||
frame = ttk.LabelFrame(parent, text="Подключение", padding="10")
|
||||
frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
ttk.Label(frame, text="COM порт:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
self.port_var = tk.StringVar(value="/dev/ttyACM0")
|
||||
port_combo = ttk.Combobox(frame, textvariable=self.port_var, width=20, state="readonly")
|
||||
port_combo.grid(row=0, column=1, padx=5, pady=5)
|
||||
self.port_combo = port_combo
|
||||
|
||||
ttk.Button(frame, text="Обновить", command=self.update_serial_ports).grid(
|
||||
row=0, column=2, padx=5, pady=5
|
||||
)
|
||||
|
||||
self.connect_btn = ttk.Button(
|
||||
frame, text="Подключиться", command=self.toggle_connection
|
||||
)
|
||||
self.connect_btn.grid(row=1, column=0, columnspan=3, pady=10, sticky=(tk.W, tk.E))
|
||||
|
||||
self.status_label = ttk.Label(frame, text="Отключено", foreground="red")
|
||||
self.status_label.grid(row=2, column=0, columnspan=3, pady=5)
|
||||
|
||||
def setup_info_panel(self, parent):
|
||||
"""Панель информации об устройстве"""
|
||||
frame = ttk.LabelFrame(parent, text="Информация об устройстве", padding="10")
|
||||
frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
self.info_labels = {}
|
||||
info_fields = [
|
||||
("modelName", "Модель:"),
|
||||
("hardwareVersion", "Версия железа:"),
|
||||
("firmwareVersion", "Версия прошивки:"),
|
||||
]
|
||||
|
||||
for i, (key, label) in enumerate(info_fields):
|
||||
ttk.Label(frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
|
||||
value_label = ttk.Label(frame, text="---", foreground="gray")
|
||||
value_label.grid(row=i, column=1, sticky=tk.W, padx=10)
|
||||
self.info_labels[key] = value_label
|
||||
|
||||
def setup_control_panel(self, parent):
|
||||
"""Панель управления"""
|
||||
frame = ttk.LabelFrame(parent, text="Управление выходом", padding="10")
|
||||
frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
# Текущие значения
|
||||
values_frame = ttk.Frame(frame)
|
||||
values_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
self.value_labels = {}
|
||||
value_fields = [
|
||||
("inputVoltage", "Входное напряжение:", "V"),
|
||||
("outputVoltage", "Выходное напряжение:", "V"),
|
||||
("outputCurrent", "Выходной ток:", "A"),
|
||||
("outputPower", "Выходная мощность:", "W"),
|
||||
("temperature", "Температура:", "°C"),
|
||||
("outputCapacity", "Ёмкость:", "Ah"),
|
||||
("outputEnergy", "Энергия:", "Wh"),
|
||||
]
|
||||
|
||||
for i, (key, label, unit) in enumerate(value_fields):
|
||||
ttk.Label(values_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
|
||||
value_label = ttk.Label(
|
||||
values_frame, text="---", font=("Arial", 10, "bold"), foreground="blue"
|
||||
)
|
||||
value_label.grid(row=i, column=1, sticky=tk.W, padx=10)
|
||||
self.value_labels[key] = (value_label, unit)
|
||||
|
||||
# Установка значений
|
||||
set_frame = ttk.Frame(frame)
|
||||
set_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
|
||||
ttk.Label(set_frame, text="Установить напряжение (V):").grid(
|
||||
row=0, column=0, sticky=tk.W, pady=5
|
||||
)
|
||||
self.voltage_var = tk.StringVar(value="0.0")
|
||||
voltage_entry = ttk.Entry(set_frame, textvariable=self.voltage_var, width=10)
|
||||
voltage_entry.grid(row=0, column=1, padx=5, pady=5)
|
||||
|
||||
ttk.Button(
|
||||
set_frame, text="Установить", command=self.set_voltage
|
||||
).grid(row=0, column=2, padx=5, pady=5)
|
||||
|
||||
ttk.Label(set_frame, text="Установить ток (A):").grid(
|
||||
row=1, column=0, sticky=tk.W, pady=5
|
||||
)
|
||||
self.current_var = tk.StringVar(value="0.0")
|
||||
current_entry = ttk.Entry(set_frame, textvariable=self.current_var, width=10)
|
||||
current_entry.grid(row=1, column=1, padx=5, pady=5)
|
||||
|
||||
ttk.Button(
|
||||
set_frame, text="Установить", command=self.set_current
|
||||
).grid(row=1, column=2, padx=5, pady=5)
|
||||
|
||||
# Кнопка включения/выключения
|
||||
self.output_btn = ttk.Button(
|
||||
frame, text="Включить выход", command=self.toggle_output, state="disabled"
|
||||
)
|
||||
self.output_btn.grid(row=2, column=0, columnspan=2, pady=10, sticky=(tk.W, tk.E))
|
||||
|
||||
# Режим работы
|
||||
mode_frame = ttk.Frame(frame)
|
||||
mode_frame.grid(row=3, column=0, columnspan=2, pady=5)
|
||||
|
||||
ttk.Label(mode_frame, text="Режим:").grid(row=0, column=0, sticky=tk.W)
|
||||
self.mode_label = ttk.Label(
|
||||
mode_frame, text="---", font=("Arial", 10, "bold"), foreground="green"
|
||||
)
|
||||
self.mode_label.grid(row=0, column=1, padx=10)
|
||||
|
||||
# Состояние защиты
|
||||
ttk.Label(mode_frame, text="Защита:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
self.protection_label = ttk.Label(
|
||||
mode_frame, text="---", font=("Arial", 10, "bold"), foreground="orange"
|
||||
)
|
||||
self.protection_label.grid(row=1, column=1, padx=10, pady=5)
|
||||
|
||||
def setup_protection_panel(self, parent):
|
||||
"""Панель настроек защиты"""
|
||||
frame = ttk.LabelFrame(parent, text="Настройки защиты", padding="10")
|
||||
frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
self.protection_vars = {}
|
||||
protection_fields = [
|
||||
(OVP, "overVoltageProtection", "OVP (V):"),
|
||||
(OCP, "overCurrentProtection", "OCP (A):"),
|
||||
(OPP, "overPowerProtection", "OPP (W):"),
|
||||
(OTP, "overTemperatureProtection", "OTP (°C):"),
|
||||
(LVP, "lowVoltageProtection", "LVP (V):"),
|
||||
]
|
||||
|
||||
for i, (cmd, key, label) in enumerate(protection_fields):
|
||||
ttk.Label(frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
|
||||
var = tk.StringVar(value="0.0")
|
||||
entry = ttk.Entry(frame, textvariable=var, width=10)
|
||||
entry.grid(row=i, column=1, padx=5, pady=2)
|
||||
|
||||
def make_setter(cmd_val, var_ref=var):
|
||||
return lambda: self.set_protection(cmd_val, var_ref.get())
|
||||
|
||||
ttk.Button(frame, text="Уст.", command=make_setter(cmd)).grid(
|
||||
row=i, column=2, padx=5, pady=2
|
||||
)
|
||||
self.protection_vars[key] = var
|
||||
|
||||
def setup_log_panel(self, parent):
|
||||
"""Панель логов"""
|
||||
frame = ttk.LabelFrame(parent, text="Лог", padding="10")
|
||||
frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
parent.rowconfigure(4, weight=1)
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(
|
||||
frame, height=8, width=40, wrap=tk.WORD
|
||||
)
|
||||
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
|
||||
def setup_graphs_panel(self, parent):
|
||||
"""Панель графиков"""
|
||||
frame = ttk.Frame(parent)
|
||||
frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(0, weight=1)
|
||||
|
||||
# Создаём фигуру с тремя подграфиками
|
||||
self.fig = Figure(figsize=(10, 8), dpi=100)
|
||||
|
||||
# График напряжения
|
||||
self.ax_voltage = self.fig.add_subplot(311)
|
||||
self.ax_voltage.set_title("Выходное напряжение", fontsize=10)
|
||||
self.ax_voltage.set_ylabel("Напряжение (V)", fontsize=9)
|
||||
self.ax_voltage.grid(True)
|
||||
self.line_voltage, = self.ax_voltage.plot([], [], 'b-', lw=1.5, label="Напряжение")
|
||||
self.ax_voltage.legend(fontsize=8)
|
||||
|
||||
# График тока
|
||||
self.ax_current = self.fig.add_subplot(312)
|
||||
self.ax_current.set_title("Выходной ток", fontsize=10)
|
||||
self.ax_current.set_ylabel("Ток (A)", fontsize=9)
|
||||
self.ax_current.grid(True)
|
||||
self.line_current, = self.ax_current.plot([], [], 'r-', lw=1.5, label="Ток")
|
||||
self.ax_current.legend(fontsize=8)
|
||||
|
||||
# График мощности
|
||||
self.ax_power = self.fig.add_subplot(313)
|
||||
self.ax_power.set_title("Выходная мощность", fontsize=10)
|
||||
self.ax_power.set_xlabel("Время (сек)", fontsize=9)
|
||||
self.ax_power.set_ylabel("Мощность (W)", fontsize=9)
|
||||
self.ax_power.grid(True)
|
||||
self.line_power, = self.ax_power.plot([], [], 'g-', lw=1.5, label="Мощность")
|
||||
self.ax_power.legend(fontsize=8)
|
||||
|
||||
self.fig.tight_layout()
|
||||
|
||||
# Встраиваем в tkinter
|
||||
self.canvas = FigureCanvasTkAgg(self.fig, master=frame)
|
||||
self.canvas.draw()
|
||||
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Запускаем анимацию
|
||||
self.ani = animation.FuncAnimation(
|
||||
self.fig, self.update_graphs, interval=200, blit=False, cache_frame_data=False
|
||||
)
|
||||
|
||||
def log(self, message):
|
||||
"""Добавить сообщение в лог"""
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
|
||||
self.log_text.see(tk.END)
|
||||
|
||||
def update_serial_ports(self):
|
||||
"""Обновить список доступных COM портов"""
|
||||
ports = [port.device for port in serial.tools.list_ports.comports()]
|
||||
if not ports:
|
||||
ports = ["/dev/ttyACM0", "/dev/ttyUSB0", "COM1", "COM3"]
|
||||
self.port_combo['values'] = ports
|
||||
if ports and not self.port_var.get() in ports:
|
||||
self.port_var.set(ports[0])
|
||||
|
||||
def toggle_connection(self):
|
||||
"""Подключение/отключение от устройства"""
|
||||
if not self.connected:
|
||||
self.connect()
|
||||
else:
|
||||
self.disconnect()
|
||||
|
||||
def connect(self):
|
||||
"""Подключиться к устройству"""
|
||||
port = self.port_var.get()
|
||||
if not port:
|
||||
messagebox.showerror("Ошибка", "Выберите COM порт")
|
||||
return
|
||||
|
||||
try:
|
||||
self.log(f"Подключение к {port}...")
|
||||
self.device = DPS150(port, callback=self.on_data_received)
|
||||
self.device.start()
|
||||
time.sleep(0.5)
|
||||
self.device.get_all()
|
||||
|
||||
self.connected = True
|
||||
self.connect_btn.config(text="Отключиться")
|
||||
self.status_label.config(text="Подключено", foreground="green")
|
||||
self.output_btn.config(state="normal")
|
||||
self.log("Успешно подключено")
|
||||
# Запускаем периодическое обновление данных
|
||||
self.schedule_data_update()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка подключения", str(e))
|
||||
self.log(f"Ошибка подключения: {e}")
|
||||
if self.device:
|
||||
try:
|
||||
self.device.stop()
|
||||
except:
|
||||
pass
|
||||
self.device = None
|
||||
|
||||
def disconnect(self):
|
||||
"""Отключиться от устройства"""
|
||||
try:
|
||||
if self.device:
|
||||
if self.device._running:
|
||||
self.device.disable()
|
||||
self.device.stop()
|
||||
self.connected = False
|
||||
self.connect_btn.config(text="Подключиться")
|
||||
self.status_label.config(text="Отключено", foreground="red")
|
||||
self.output_btn.config(state="disabled")
|
||||
self.log("Отключено")
|
||||
self.device = None
|
||||
# Останавливаем периодическое обновление
|
||||
if hasattr(self, 'update_job'):
|
||||
self.root.after_cancel(self.update_job)
|
||||
except Exception as e:
|
||||
self.log(f"Ошибка при отключении: {e}")
|
||||
|
||||
def on_data_received(self, data):
|
||||
"""Обработка полученных данных от устройства"""
|
||||
self.data.update(data)
|
||||
self.root.after(0, self.update_ui)
|
||||
|
||||
def update_ui(self):
|
||||
"""Обновление интерфейса на основе полученных данных"""
|
||||
# Информация об устройстве
|
||||
for key, label in self.info_labels.items():
|
||||
value = self.data.get(key, "---")
|
||||
label.config(text=str(value) if value != "---" else "---")
|
||||
|
||||
# Текущие значения
|
||||
for key, (label, unit) in self.value_labels.items():
|
||||
value = self.data.get(key)
|
||||
if value is not None:
|
||||
label.config(text=f"{value:.3f} {unit}")
|
||||
else:
|
||||
label.config(text="---")
|
||||
|
||||
# Режим работы
|
||||
mode = self.data.get("mode", "---")
|
||||
if mode != "---":
|
||||
self.mode_label.config(text=mode)
|
||||
|
||||
# Состояние защиты
|
||||
protection = self.data.get("protectionState", "---")
|
||||
if protection and protection != "---":
|
||||
self.protection_label.config(text=protection, foreground="red")
|
||||
else:
|
||||
self.protection_label.config(text="Норма", foreground="green")
|
||||
|
||||
# Обновление данных для графиков
|
||||
current_time = time.time() - self.start_time
|
||||
voltage = self.data.get("outputVoltage")
|
||||
current = self.data.get("outputCurrent")
|
||||
power = self.data.get("outputPower")
|
||||
|
||||
if voltage is not None:
|
||||
self.time_data.append(current_time)
|
||||
self.voltage_data.append(voltage)
|
||||
self.current_data.append(current if current is not None else 0)
|
||||
self.power_data.append(power if power is not None else 0)
|
||||
|
||||
# Обновление значений защиты в полях ввода
|
||||
for key, var in self.protection_vars.items():
|
||||
value = self.data.get(key)
|
||||
if value is not None and not var.get():
|
||||
var.set(f"{value:.2f}")
|
||||
|
||||
# Обновление кнопки выхода
|
||||
self._update_output_button()
|
||||
|
||||
def update_graphs(self, frame):
|
||||
"""Обновление графиков"""
|
||||
if len(self.time_data) > 0:
|
||||
time_list = list(self.time_data)
|
||||
|
||||
# Обновление графика напряжения
|
||||
if len(self.voltage_data) > 0:
|
||||
self.line_voltage.set_data(time_list, list(self.voltage_data))
|
||||
self.ax_voltage.relim()
|
||||
self.ax_voltage.autoscale_view()
|
||||
|
||||
# Обновление графика тока
|
||||
if len(self.current_data) > 0:
|
||||
self.line_current.set_data(time_list, list(self.current_data))
|
||||
self.ax_current.relim()
|
||||
self.ax_current.autoscale_view()
|
||||
|
||||
# Обновление графика мощности
|
||||
if len(self.power_data) > 0:
|
||||
self.line_power.set_data(time_list, list(self.power_data))
|
||||
self.ax_power.relim()
|
||||
self.ax_power.autoscale_view()
|
||||
|
||||
return [self.line_voltage, self.line_current, self.line_power]
|
||||
|
||||
def set_voltage(self):
|
||||
"""Установить напряжение"""
|
||||
if not self.connected or not self.device:
|
||||
messagebox.showwarning("Предупреждение", "Сначала подключитесь к устройству")
|
||||
return
|
||||
|
||||
try:
|
||||
value = float(self.voltage_var.get())
|
||||
self.device.set_float_value(VOLTAGE_SET, value)
|
||||
self.log(f"Установлено напряжение: {value} V")
|
||||
except ValueError:
|
||||
messagebox.showerror("Ошибка", "Неверное значение напряжения")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось установить напряжение: {e}")
|
||||
self.log(f"Ошибка установки напряжения: {e}")
|
||||
|
||||
def set_current(self):
|
||||
"""Установить ток"""
|
||||
if not self.connected or not self.device:
|
||||
messagebox.showwarning("Предупреждение", "Сначала подключитесь к устройству")
|
||||
return
|
||||
|
||||
try:
|
||||
value = float(self.current_var.get())
|
||||
self.device.set_float_value(CURRENT_SET, value)
|
||||
self.log(f"Установлен ток: {value} A")
|
||||
except ValueError:
|
||||
messagebox.showerror("Ошибка", "Неверное значение тока")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось установить ток: {e}")
|
||||
self.log(f"Ошибка установки тока: {e}")
|
||||
|
||||
def _update_output_button(self):
|
||||
"""Обновить текст кнопки выхода на основе текущего состояния"""
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
output_closed = self.data.get("outputClosed", False)
|
||||
if output_closed:
|
||||
self.output_btn.config(text="Включить выход")
|
||||
else:
|
||||
self.output_btn.config(text="Выключить выход")
|
||||
|
||||
def toggle_output(self):
|
||||
"""Включить/выключить выход"""
|
||||
if not self.connected or not self.device:
|
||||
return
|
||||
|
||||
try:
|
||||
output_closed = self.data.get("outputClosed", False)
|
||||
if output_closed:
|
||||
self.device.enable()
|
||||
self.log("Выход включен")
|
||||
else:
|
||||
self.device.disable()
|
||||
self.log("Выход выключен")
|
||||
# Обновление произойдёт при следующем получении данных
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось изменить состояние выхода: {e}")
|
||||
self.log(f"Ошибка изменения состояния выхода: {e}")
|
||||
|
||||
def set_protection(self, cmd, value_str):
|
||||
"""Установить значение защиты"""
|
||||
if not self.connected or not self.device:
|
||||
messagebox.showwarning("Предупреждение", "Сначала подключитесь к устройству")
|
||||
return
|
||||
|
||||
try:
|
||||
value = float(value_str)
|
||||
self.device.set_float_value(cmd, value)
|
||||
protection_names = {
|
||||
OVP: "OVP", OCP: "OCP", OPP: "OPP", OTP: "OTP", LVP: "LVP"
|
||||
}
|
||||
name = protection_names.get(cmd, "Защита")
|
||||
self.log(f"Установлена {name}: {value}")
|
||||
except ValueError:
|
||||
messagebox.showerror("Ошибка", "Неверное значение")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось установить защиту: {e}")
|
||||
self.log(f"Ошибка установки защиты: {e}")
|
||||
|
||||
def schedule_data_update(self):
|
||||
"""Планировать периодическое обновление данных"""
|
||||
if self.connected and self.device:
|
||||
try:
|
||||
self.device.get_all()
|
||||
except:
|
||||
pass
|
||||
if self.connected:
|
||||
self.update_job = self.root.after(1000, self.schedule_data_update) # Обновление каждую секунду
|
||||
|
||||
def on_closing(self):
|
||||
"""Обработка закрытия окна"""
|
||||
if self.connected:
|
||||
self.disconnect()
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = DPS150GUI(root)
|
||||
root.protocol("WM_DELETE_WINDOW", app.on_closing)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user