Files
ComConfigCopy/ComConfigCopy.py
LowaSC ea432d2893 Add configuration file selection and management window
- Implement ConfigSelectorWindow for advanced configuration file selection
- Add drag-and-drop support for configuration files
- Create file management features: add, edit, rename, and delete configs
- Enhance file selection process with a tree-view interface
- Improve configuration file handling with user-friendly interactions
2025-02-19 22:25:28 +03:00

2332 lines
109 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ------------------------------------------------------------
# Это программа для копирования конфигураций на коммутаторы
# ------------------------------------------------------------
import json
import logging
import os
import re
import sys
import threading
import time
import webbrowser
from getpass import getpass
from logging.handlers import RotatingFileHandler
import tkinter as tk
from tkinter import (
StringVar,
END,
BOTH,
LEFT,
RIGHT,
X,
W,
filedialog,
messagebox,
simpledialog,
Y,
)
from tkinter import ttk
import serial
import serial.tools.list_ports
from serial.serialutil import SerialException
from about_window import AboutWindow
from TFTPServer import TFTPServer
import socket
from update_checker import UpdateChecker
# Версия программы
VERSION = "1.0.2"
# Создаем необходимые папки
os.makedirs("Logs", exist_ok=True)
os.makedirs("Configs", exist_ok=True)
os.makedirs("Settings", exist_ok=True)
os.makedirs("Firmware", exist_ok=True)
os.makedirs("docs", exist_ok=True)
# Файл настроек находится в папке Settings
SETTINGS_FILE = os.path.join("Settings", "settings.json")
# Настройка логирования с использованием RotatingFileHandler.
def setup_logging():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
log_path = os.path.join("Logs", "app.log")
handler = RotatingFileHandler(
log_path, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
# Загрузка настроек из JSON-файла или создание настроек по умолчанию.
def settings_load():
default_settings = {
"port": None, # Порт для подключения
"baudrate": 9600, # Скорость передачи данных
"config_file": None, # Файл конфигурации
"login": None, # Логин для подключения
"password": None, # Пароль для подключения
"timeout": 10, # Таймаут подключения
"copy_mode": "line", # Режим копирования
"block_size": 15, # Размер блока команд
"prompt": ">", # Используется для определения приглашения
}
# Создаем папку Settings, если её нет
os.makedirs("Settings", exist_ok=True)
if not os.path.exists(SETTINGS_FILE):
try:
# При первом запуске создаем новый файл настроек
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(default_settings, f, indent=4, ensure_ascii=False)
logging.info("Файл настроек создан с настройками по умолчанию.")
return default_settings
except Exception as e:
logging.error(f"Ошибка при создании файла настроек: {e}", exc_info=True)
return default_settings
try:
# Пытаемся загрузить существующие настройки
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
settings = json.load(f)
# Проверяем наличие всех необходимых параметров
settings_changed = False
for key, value in default_settings.items():
if key not in settings:
settings[key] = value
settings_changed = True
# Если были добавлены новые параметры, сохраняем обновленные настройки
if settings_changed:
try:
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=4, ensure_ascii=False)
logging.info("Файл настроек обновлен с новыми параметрами.")
except Exception as e:
logging.error(f"Ошибка при обновлении файла настроек: {e}", exc_info=True)
return settings
except Exception as e:
logging.error(f"Ошибка загрузки файла настроек: {e}", exc_info=True)
return default_settings
# Сохранение настроек в JSON-файл
def settings_save(settings):
try:
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=4, ensure_ascii=False)
logging.info("Настройки сохранены в файл.")
except Exception as e:
logging.error(f"Ошибка при сохранении настроек: {e}", exc_info=True)
# Получение списка доступных последовательных портов
def list_serial_ports():
"""Получение списка доступных последовательных портов."""
ports = serial.tools.list_ports.comports()
logging.debug(f"Найдено {len(ports)} серийных портов.")
return [port.device for port in ports]
# Получение списка IP-адресов из сетевых адаптеров
def get_network_adapters():
adapters = []
try:
# Получаем имя хоста
hostname = socket.gethostname()
# Получаем все адреса для данного хоста
addresses = socket.getaddrinfo(hostname, None)
# Создаем множество для хранения уникальных IP-адресов
unique_ips = set()
for addr in addresses:
ip = addr[4][0]
# Пропускаем IPv6 и локальные адреса
if ':' not in ip and not ip.startswith('127.'):
unique_ips.add(ip)
# Добавляем все найденные IP-адреса в список
for ip in sorted(unique_ips):
adapters.append(f"{ip}")
# Добавляем 0.0.0.0 для прослушивания всех интерфейсов
adapters.insert(0, "0.0.0.0")
except Exception as e:
logging.error(f"Ошибка при получении списка сетевых адаптеров: {e}", exc_info=True)
# В случае ошибки возвращаем хотя бы 0.0.0.0
adapters = ["0.0.0.0"]
return adapters
# Создание соединения с устройством через последовательный порт
def create_connection(settings):
try:
conn = serial.Serial(settings["port"], settings["baudrate"], timeout=1)
logging.info(f"Соединение установлено с {settings['port']} на {settings['baudrate']}.")
time.sleep(1)
return conn
except SerialException as e:
logging.error(f"Ошибка подключения: {e}", exc_info=True)
except Exception as e:
logging.error(f"Неизвестная ошибка подключения: {e}", exc_info=True)
return None
# Проверка наличия логина и пароля в настройках и отправка их на устройство
def send_login_password(serial_connection, login=None, password=None, is_gui=False):
if not login:
if is_gui:
login = simpledialog.askstring("Login", "Введите логин:")
if login is None:
login = ""
else:
login = input("Введите логин: ")
if not password:
if is_gui:
password = simpledialog.askstring("Password", "Введите пароль:", show="*")
if password is None:
password = ""
else:
password = getpass("Введите пароль: ")
# Чтение ответа от устройства с учётом таймаута.
def read_response(serial_connection, timeout, login=None, password=None, is_gui=False):
response = b""
end_time = time.time() + timeout
decoded = ""
while time.time() < end_time:
if serial_connection.in_waiting:
chunk = serial_connection.read(serial_connection.in_waiting)
response += chunk
if b"--More--" in response:
serial_connection.write(b"\n")
response = response.replace(b"--More--", b"")
try:
decoded = response.decode(errors="ignore")
except Exception:
decoded = ""
lines = decoded.rstrip().splitlines()
if lines:
last_line = lines[-1].strip()
if re.search(r'(login:|username:)$', last_line, re.IGNORECASE):
send_login_password(serial_connection, login, None, is_gui)
response = b""
continue
if re.search(r'(password:)$', last_line, re.IGNORECASE):
send_login_password(serial_connection, None, password, is_gui)
response = b""
continue
if last_line.endswith(">") or last_line.endswith("#"):
break
else:
time.sleep(0.1)
return decoded
# Генерация блоков команд для блочного копирования
def generate_command_blocks(lines, block_size):
blocks = []
current_block = []
for line in lines:
trimmed = line.strip()
if not trimmed:
continue
lower_line = trimmed.lower()
if lower_line.startswith("vlan") or lower_line.startswith("enable") or lower_line.startswith("interface"):
if current_block:
blocks.append("\n".join(current_block))
current_block = []
blocks.append(trimmed)
elif lower_line.startswith("exit"):
current_block.append(trimmed)
blocks.append("\n".join(current_block))
current_block = []
else:
current_block.append(trimmed)
if len(current_block) >= block_size:
blocks.append("\n".join(current_block))
current_block = []
if current_block:
blocks.append("\n".join(current_block))
return blocks
# Выполнение команд из файла конфигурации
def execute_commands_from_file(
serial_connection,
filename,
timeout,
copy_mode,
block_size,
log_callback=None,
login=None,
password=None,
is_gui=False,
):
try:
with open(filename, "r", encoding="utf-8") as file:
lines = [line for line in file if line.strip()]
msg = f"Выполнение команд из файла: {filename}\n"
logging.info(msg)
if log_callback:
log_callback(msg)
# Если выбран построчный режим
if copy_mode == "line":
for cmd in lines:
cmd = cmd.strip()
max_attempts = 3
attempt = 0
while attempt < max_attempts:
msg = f"[CMD] {cmd}" # Изменено форматирование для команды
if log_callback:
log_callback(msg)
serial_connection.write((cmd + "\n").encode())
logging.info(f"Отправлена команда: {cmd} (Попытка {attempt+1})")
response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui)
if response:
if '^' in response:
msg = (
f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n"
f"Ответ устройства:\n{response}\n"
f"Повторная отправка команды...\n"
)
if log_callback:
log_callback(msg)
logging.warning(f"Ошибка в команде: {cmd}. Повторная отправка.")
attempt += 1
time.sleep(1)
continue
else:
msg = f"Ответ устройства:\n{response}\n"
if log_callback:
log_callback(msg)
logging.info(f"Ответ устройства:\n{response}")
break
else:
msg = f"Ответ не получен для команды: {cmd}\n"
if log_callback:
log_callback(msg)
logging.warning(f"Нет ответа для команды: {cmd}")
break
if attempt == max_attempts:
msg = f"[ERROR] Команда не выполнена корректно после {max_attempts} попыток: {cmd}\n"
if log_callback:
log_callback(msg)
logging.error(msg)
time.sleep(1)
# Если выбран блочный режим
elif copy_mode == "block":
blocks = generate_command_blocks(lines, block_size)
for block in blocks:
msg = f"[CMD] Отправка блока команд:\n{block}" # Изменено форматирование для блока команд
if log_callback:
log_callback(msg)
serial_connection.write((block + "\n").encode())
logging.info(f"Отправлен блок команд:\n{block}")
response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui)
# Если обнаружена ошибка в ответе на блок, отправляем команды по очереди
if response and '^' in response:
msg = (
f"[WARNING] Обнаружена ошибка при выполнении блока команд.\n"
f"Ответ устройства:\n{response}\n"
f"Пересылаются команды по отдельности...\n"
)
if log_callback:
log_callback(msg)
logging.warning("Ошибка в блочном режиме отправляем команды индивидуально.")
for line in block.splitlines():
cmd = line.strip()
if not cmd:
continue
max_attempts = 3
attempt = 0
while attempt < max_attempts:
sub_msg = f"[CMD] {cmd}" # Изменено форматирование для команды
if log_callback:
log_callback(sub_msg)
serial_connection.write((cmd + "\n").encode())
logging.info(f"Отправлена команда: {cmd} (Попытка {attempt+1})")
sub_response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui)
if sub_response:
if '^' in sub_response:
sub_msg = (
f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n"
f"Ответ устройства:\n{sub_response}\n"
f"Повторная отправка команды...\n"
)
if log_callback:
log_callback(sub_msg)
logging.warning(f"Ошибка в команде: {cmd}. Повторная отправка.")
attempt += 1
time.sleep(1)
continue
else:
sub_msg = f"Ответ устройства:\n{sub_response}\n"
if log_callback:
log_callback(sub_msg)
logging.info(f"Ответ устройства:\n{sub_response}")
break
else:
sub_msg = f"Ответ не получен для команды: {cmd}\n"
if log_callback:
log_callback(sub_msg)
logging.warning(f"Нет ответа для команды: {cmd}")
break
if attempt == max_attempts:
sub_msg = f"[ERROR] Команда не выполнена корректно после {max_attempts} попыток: {cmd}\n"
if log_callback:
log_callback(sub_msg)
logging.error(sub_msg)
time.sleep(1)
else:
if response:
msg = f"Ответ устройства:\n{response}\n"
if log_callback:
log_callback(msg)
logging.info(f"Ответ устройства:\n{response}")
else:
msg = f"Ответ не получен для блока:\n{block}\n"
if log_callback:
log_callback(msg)
logging.warning(f"Нет ответа для блока:\n{block}")
time.sleep(1)
except SerialException as e:
msg = f"Ошибка при выполнении команды: {e}\n"
if log_callback:
log_callback(msg)
logging.error(f"Ошибка при выполнении команды: {e}", exc_info=True)
except Exception as e:
msg = f"Ошибка при выполнении команд: {e}\n"
if log_callback:
log_callback(msg)
logging.error(f"Ошибка при выполнении команд из файла: {e}", exc_info=True)
# Базовый класс для кастомных виджетов с поддержкой контекстного меню и горячих клавиш
class CustomWidgetBase:
def __init__(self):
self.create_context_menu()
self.bind_shortcuts()
# Создание контекстного меню
def create_context_menu(self):
"""Создание контекстного меню"""
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="Вырезать", command=self.cut)
self.context_menu.add_command(label="Копировать", command=self.copy)
self.context_menu.add_command(label="Вставить", command=self.paste)
self.context_menu.add_separator()
self.context_menu.add_command(label="Выделить всё", command=self.select_all)
self.bind("<Button-3>", self.show_context_menu)
# Привязка горячих клавиш
def bind_shortcuts(self):
"""Привязка горячих клавиш"""
# Стандартные сочетания
self.bind("<Control-x>", lambda e: self.event_generate("<<Cut>>"))
self.bind("<Control-c>", lambda e: self.event_generate("<<Copy>>"))
self.bind("<Control-v>", lambda e: self.event_generate("<<Paste>>"))
self.bind("<Control-a>", self.select_all)
# Дополнительные сочетания
self.bind("<Shift-Insert>", lambda e: self.event_generate("<<Paste>>"))
self.bind("<Control-Insert>", lambda e: self.event_generate("<<Copy>>"))
self.bind("<Shift-Delete>", lambda e: self.event_generate("<<Cut>>"))
# Отображение контекстного меню
def show_context_menu(self, event):
"""Отображение контекстного меню"""
self.context_menu.post(event.x_root, event.y_root)
# Вырезание текста
def cut(self):
self.event_generate("<<Cut>>")
# Копирование текста
def copy(self):
self.event_generate("<<Copy>>")
# Вставка текста
def paste(self):
self.event_generate("<<Paste>>")
# Класс для текстового поля с поддержкой контекстного меню и горячих клавиш
class CustomText(tk.Text, CustomWidgetBase):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
CustomWidgetBase.__init__(self)
def select_all(self, event=None):
"""Выделение всего текста в Text виджете"""
self.tag_add(tk.SEL, "1.0", tk.END)
self.mark_set(tk.INSERT, "1.0")
self.see(tk.INSERT)
return "break"
# Класс для поля ввода с поддержкой контекстного меню и горячих клавиш
class CustomEntry(ttk.Entry, CustomWidgetBase):
def __init__(self, *args, **kwargs):
ttk.Entry.__init__(self, *args, **kwargs)
CustomWidgetBase.__init__(self)
def select_all(self, event=None):
"""Выделение всего текста в Entry виджете"""
self.select_range(0, tk.END)
return "break"
# Класс для окна настроек
class SettingsWindow(tk.Toplevel):
def __init__(self, parent, settings, callback=None):
super().__init__(parent)
self.title("Настройки")
self.geometry("600x400")
self.settings = settings
self.callback = callback
self.resizable(False, False)
# Создаем фрейм для настроек
settings_frame = ttk.Frame(self, padding="10")
settings_frame.pack(fill=BOTH, expand=True)
# COM порт
ttk.Label(settings_frame, text="COM порт:").grid(row=0, column=0, sticky=W, pady=5)
self.port_var = StringVar(value=settings.get("port", ""))
self.port_combo = ttk.Combobox(settings_frame, textvariable=self.port_var)
self.port_combo.grid(row=0, column=1, sticky=W, pady=5)
ttk.Button(settings_frame, text="Обновить", command=self.update_ports).grid(row=0, column=2, padx=5)
# Скорость передачи
ttk.Label(settings_frame, text="Скорость:").grid(row=1, column=0, sticky=W, pady=5)
self.baudrate_var = StringVar(value=str(settings.get("baudrate", 9600)))
baudrate_combo = ttk.Combobox(settings_frame, textvariable=self.baudrate_var,
values=["9600", "19200", "38400", "57600", "115200"])
baudrate_combo.grid(row=1, column=1, sticky=W, pady=5)
# Таймаут
ttk.Label(settings_frame, text="Таймаут (сек):").grid(row=2, column=0, sticky=W, pady=5)
self.timeout_var = StringVar(value=str(settings.get("timeout", 10)))
timeout_entry = ttk.Entry(settings_frame, textvariable=self.timeout_var)
timeout_entry.grid(row=2, column=1, sticky=W, pady=5)
# Режим копирования
ttk.Label(settings_frame, text="Режим копирования:").grid(row=3, column=0, sticky=W, pady=5)
self.copy_mode_var = StringVar(value=settings.get("copy_mode", "line"))
copy_mode_frame = ttk.Frame(settings_frame)
copy_mode_frame.grid(row=3, column=1, sticky=W, pady=5)
ttk.Radiobutton(copy_mode_frame, text="Построчно", value="line",
variable=self.copy_mode_var, command=self.toggle_block_size).pack(side=LEFT)
ttk.Radiobutton(copy_mode_frame, text="Блоками", value="block",
variable=self.copy_mode_var, command=self.toggle_block_size).pack(side=LEFT)
# Размер блока
self.block_size_label = ttk.Label(settings_frame, text="Размер блока:")
self.block_size_label.grid(row=4, column=0, sticky=W, pady=5)
self.block_size_var = StringVar(value=str(settings.get("block_size", 15)))
self.block_size_entry = CustomEntry(settings_frame, textvariable=self.block_size_var)
self.block_size_entry.grid(row=4, column=1, sticky=W, pady=5)
# Приглашение командной строки
ttk.Label(settings_frame, text="Приглашение:").grid(row=5, column=0, sticky=W, pady=5)
self.prompt_var = StringVar(value=settings.get("prompt", ">"))
prompt_entry = CustomEntry(settings_frame, textvariable=self.prompt_var)
prompt_entry.grid(row=5, column=1, sticky=W, pady=5)
# Кнопки
button_frame = ttk.Frame(settings_frame)
button_frame.grid(row=6, column=0, columnspan=3, pady=20)
ttk.Button(button_frame, text="Сохранить", command=self.save_settings).pack(side=LEFT, padx=5)
ttk.Button(button_frame, text="Отмена", command=self.destroy).pack(side=LEFT, padx=5)
self.update_ports()
# Инициализация видимости поля размера блока
self.toggle_block_size()
# Центрируем окно
self.center_window()
# Переключение видимости поля размера блока
def toggle_block_size(self):
if self.copy_mode_var.get() == "line":
self.block_size_label.grid_remove()
self.block_size_entry.grid_remove()
else:
self.block_size_label.grid()
self.block_size_entry.grid()
# Центрирование окна
def center_window(self):
"""Центрирование окна на экране"""
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
# Обновление списка доступных последовательных портов
def update_ports(self):
ports = list_serial_ports()
self.port_combo["values"] = ports
if ports and not self.port_var.get():
self.port_var.set(ports[0])
# Сохранение настроек
def save_settings(self):
try:
self.settings.update({
"port": self.port_var.get(),
"baudrate": int(self.baudrate_var.get()),
"timeout": int(self.timeout_var.get()),
"copy_mode": self.copy_mode_var.get(),
"block_size": int(self.block_size_var.get()),
"prompt": self.prompt_var.get()
})
settings_save(self.settings)
if self.callback:
self.callback()
self.destroy()
messagebox.showinfo("Успех", "Настройки успешно сохранены")
except ValueError as e:
messagebox.showerror("Ошибка", "Проверьте правильность введенных значений")
# Общая функция для добавления текста в текстовое поле
def append_text_to_widget(widget, text):
# Проверяем, заканчивается ли текст символом новой строки
if not text.endswith('\n'):
text += '\n'
widget.insert(END, text)
widget.see(END)
# Общая функция для выбора файла конфигурации
def select_config_file(self, var, save_to_settings=False):
filename = filedialog.askopenfilename(
title="Выберите файл конфигурации",
filetypes=[("Text files", "*.txt")]
)
if filename:
var.set(filename)
if save_to_settings:
self.settings["config_file"] = filename
settings_save(self.settings)
# Общая функция для отправки команды и обработки ответа
def send_command_and_process_response(
serial_connection,
cmd,
timeout,
max_attempts=3,
log_callback=None,
login=None,
password=None,
is_gui=False
):
attempt = 0
while attempt < max_attempts:
# Проверяем, что соединение всё ещё активно
if not serial_connection or not serial_connection.is_open:
if log_callback:
log_callback("[INFO] Соединение закрыто.\n")
return False, None
msg = f"\nОтправка команды: {cmd}\n"
if log_callback:
log_callback(msg)
serial_connection.write((cmd + "\n").encode())
logging.info(f"Отправлена команда: {cmd}")
response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui)
if response:
if '^' in response:
msg = (
f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n"
f"Ответ устройства:\n{response}\n"
f"Повторная отправка команды...\n"
)
if log_callback:
log_callback(msg)
logging.warning(f"Ошибка в команде: {cmd}. Повторная отправка.")
attempt += 1
time.sleep(1)
continue
else:
msg = f"Ответ устройства:\n{response}\n"
if log_callback:
log_callback(msg)
logging.info(f"Ответ устройства:\n{response}")
return True, response
else:
msg = f"Ответ не получен для команды: {cmd}\n"
if log_callback:
log_callback(msg)
logging.warning(f"Нет ответа для команды: {cmd}")
return False, None
msg = f"[ERROR] Команда не выполнена корректно: {cmd}\n"
if log_callback:
log_callback(msg)
logging.error(msg)
return False, None
# Класс для терминального виджета с расширенной функциональностью
class TerminalWidget(CustomText):
def __init__(self, master, *args, **kwargs):
super().__init__(master, *args, **kwargs)
# Настройка цветов для разных типов сообщений
self.tag_configure("error", foreground="red")
self.tag_configure("warning", foreground="orange")
self.tag_configure("info", foreground="blue")
self.tag_configure("command", foreground="green")
self.tag_configure("separator", foreground="gray")
# Добавляем скроллбар
self.scrollbar = ttk.Scrollbar(self, command=self.yview)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.config(yscrollcommand=self.scrollbar.set)
# Настройка отступов и переносов
self.config(
wrap=tk.WORD,
padx=5,
pady=5,
spacing1=2, # Отступ перед абзацем
spacing2=2, # Межстрочный интервал
spacing3=2, # Отступ после абзаца
state='disabled' # Делаем виджет только для чтения
)
# Счетчик команд для разделителей
self.command_counter = 0
# Отключаем возможность вставки текста
self.bind('<<Paste>>', lambda e: 'break')
self.bind('<Key>', self._readonly)
# Изменяем контекстное меню
self.context_menu.delete("Вырезать")
self.context_menu.delete("Вставить")
def _readonly(self, event):
"""Обработчик для блокировки ввода"""
return 'break'
def append_text(self, text, message_type=None):
"""
Добавление текста с определенным типом сообщения
message_type может быть: 'error', 'warning', 'info', 'command'
"""
# Временно разрешаем изменение для добавления текста
self.config(state='normal')
# Добавляем текст
if not text.endswith('\n'):
text += '\n'
start_index = self.index(tk.END)
self.insert(tk.END, text)
end_index = self.index(tk.END)
# Применяем тег в зависимости от типа сообщения
if message_type:
self.tag_add(message_type, start_index, end_index)
# Автоматическая прокрутка к концу
self.see(tk.END)
# Возвращаем состояние "только для чтения"
self.config(state='disabled')
# Обновляем виджет
self.update_idletasks()
def append_error(self, text):
"""Добавление сообщения об ошибке"""
self.append_text(text, "error")
def append_warning(self, text):
"""Добавление предупреждения"""
self.append_text(text, "warning")
def append_info(self, text):
"""Добавление информационного сообщения"""
self.append_text(text, "info")
def append_command(self, text):
"""Добавление команды с разделителем"""
# Временно разрешаем изменение для добавления текста
self.config(state='normal')
# Добавляем разделитель между командами
if self.command_counter > 0:
self.insert(tk.END, "\n" + "" * 80 + "\n", "separator")
self.command_counter += 1
# Добавляем команду
self.append_text(text, "command")
# Возвращаем состояние "только для чтения"
self.config(state='disabled')
def clear(self):
"""Очистка терминала"""
self.config(state='normal')
self.delete("1.0", tk.END)
self.config(state='disabled')
self.command_counter = 0
# Основной класс для графического интерфейса
class SerialAppGUI(tk.Tk):
def __init__(self, settings):
super().__init__()
self.title("ComConfigCopy")
self.geometry("900x700")
# Добавляем VERSION как атрибут класса
self.VERSION = VERSION
# Добавляем атрибуты для управления выполнением файла
self.execution_thread = None
self.execution_paused = False
self.execution_stop = False
# Добавляем атрибуты для таймера и прогресса
self.start_time = 0
self.elapsed_time = 0
self.timer_running = False
# Добавляем атрибут для отслеживания состояния COM-порта
self.port_monitor_thread = None
self.port_monitoring = False
# Инициализация проверки обновлений
self.update_checker = UpdateChecker(
VERSION,
"https://gitea.filow.ru/LowaSC/ComConfigCopy",
include_prereleases=False # Проверяем только стабильные версии
)
# Настройка стиля
self.style = ttk.Style(self)
self.style.theme_use("clam")
default_font = ("Segoe UI", 10)
self.option_add("*Font", default_font)
self.settings = settings
self.connection = None
self.tftp_server = None
self.create_menu()
self.create_tabs()
# Добавляем статус бар
self.create_status_bar()
# Обновляем информацию в статус баре
self.update_status_bar()
# Проверка первого запуска
self.check_first_run()
# Создание меню
def create_menu(self):
menubar = tk.Menu(self)
self.config(menu=menubar)
# Меню "Файл"
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Файл", menu=file_menu)
file_menu.add_command(label="Настройки", command=self.open_settings)
file_menu.add_separator()
file_menu.add_command(label="Выход", command=self.quit)
# Меню "Справка"
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Справка", menu=help_menu)
help_menu.add_command(label="Проверить обновления", command=self.check_for_updates)
help_menu.add_separator()
help_menu.add_command(label="О программе", command=self.open_about)
# Проверка наличия обновлений
def check_for_updates(self):
def on_update_check(update_available, error):
if error:
messagebox.showerror(
"Ошибка проверки обновлений",
f"Не удалось проверить наличие обновлений:\n{error}"
)
elif update_available:
release_info = self.update_checker.get_release_notes()
if release_info:
# Форматируем сообщение
message = (
f"Доступна новая версия {release_info['version']}!\n\n"
f"Тип релиза: {'Пре-релиз' if release_info['type'] == 'prerelease' else 'Стабильный'}\n\n"
"Изменения:\n"
f"{release_info['description']}\n\n"
"Хотите перейти на страницу загрузки?"
)
response = messagebox.askyesno(
"Доступно обновление",
message,
)
if response:
webbrowser.open(release_info["link"])
else:
messagebox.showerror(
"Ошибка",
"Не удалось получить информацию о новой версии"
)
else:
messagebox.showinfo(
"Проверка обновлений",
"У вас установлена последняя версия программы"
)
self.update_checker.check_updates(callback=on_update_check)
# Обработчик изменения настроек
def on_settings_changed(self):
self.settings = settings_load()
self.update_status_bar() # Добавляем обновление статус бара
# Открытие окна настроек
def open_settings(self):
settings_window = SettingsWindow(self, self.settings, self.on_settings_changed)
settings_window.transient(self)
settings_window.grab_set()
# Проверка первого запуска программы
def check_first_run(self):
show_settings = False
# Проверяем существование файла настроек
if not os.path.exists(SETTINGS_FILE):
show_settings = True
else:
# Проверяем содержимое файла настроек
try:
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
settings = json.load(f)
# Если порт не настроен, считаем это первым запуском
if settings.get("port") is None:
show_settings = True
except Exception:
# Если файл поврежден или не читается, тоже показываем настройки
show_settings = True
if show_settings:
# Создаем папку Settings, если её нет
os.makedirs("Settings", exist_ok=True)
response = messagebox.askyesno(
"Первый запуск",
"Это первый запуск программы. Хотите настроить параметры подключения сейчас?"
)
if response:
self.open_settings()
# Создание вкладок
def create_tabs(self):
self.notebook = ttk.Notebook(self)
self.notebook.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Создаем вкладки
interactive_frame = ttk.Frame(self.notebook)
file_exec_frame = ttk.Frame(self.notebook)
config_editor_frame = ttk.Frame(self.notebook)
tftp_frame = ttk.Frame(self.notebook)
self.notebook.add(interactive_frame, text="Интерактивный режим")
self.notebook.add(file_exec_frame, text="Выполнение файла")
self.notebook.add(config_editor_frame, text="Редактор конфигурации")
self.notebook.add(tftp_frame, text="TFTP Сервер")
self.create_interactive_tab(interactive_frame)
self.create_file_exec_tab(file_exec_frame)
self.create_config_editor_tab(config_editor_frame)
self.create_tftp_tab(tftp_frame)
# Создание вкладки "Интерактивный режим"
def create_interactive_tab(self, frame):
control_frame = ttk.Frame(frame)
control_frame.pack(fill=X, pady=5)
# Кнопка подключения с иконкой
connect_btn = ttk.Button(
control_frame,
text="⚡ Подключиться", # Unicode символ для "молнии"
command=self.connect_device
)
connect_btn.pack(side=LEFT, padx=5)
# Кнопка отключения с иконкой
disconnect_btn = ttk.Button(
control_frame,
text="✕ Отключиться", # Unicode символ для "крестика"
command=self.disconnect_device
)
disconnect_btn.pack(side=LEFT, padx=5)
# Кнопка очистки терминала
clear_btn = ttk.Button(
control_frame,
text="🗑 Очистить", # Unicode символ для "корзины"
command=lambda: self.interactive_text.clear()
)
clear_btn.pack(side=LEFT, padx=5)
# Используем новый TerminalWidget вместо CustomText
self.interactive_text = TerminalWidget(frame, height=20)
self.interactive_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
input_frame = ttk.Frame(frame)
input_frame.pack(fill=X, pady=5)
ttk.Label(input_frame, text="Команда:").pack(side=LEFT, padx=5)
self.command_entry = CustomEntry(input_frame, width=50)
self.command_entry.pack(side=LEFT, padx=5)
# Привязываем обработчик нажатия Enter к полю ввода
self.command_entry.bind('<Return>', lambda event: self.send_command())
# Кнопка отправки с иконкой
send_btn = ttk.Button(
input_frame,
text="➤ Отправить", # Unicode символ для "стрелки"
command=self.send_command
)
send_btn.pack(side=LEFT, padx=5)
# Подключение к устройству
def connect_device(self):
if self.connection:
messagebox.showinfo("Информация", "Уже подключено.")
return
if not self.settings.get("port"):
messagebox.showerror("Ошибка", "COM-порт не выбран!")
return
self.connection = create_connection(self.settings)
if self.connection:
self.interactive_text.append_info("[INFO] Подключение установлено.")
# Запускаем мониторинг состояния порта
self.start_port_monitoring()
self.update_status_bar()
else:
self.interactive_text.append_error("[ERROR] Не удалось установить соединение.")
self.update_status_bar()
# Отключение от устройства
def disconnect_device(self):
if self.connection:
try:
# Останавливаем мониторинг перед закрытием соединения
self.stop_port_monitoring()
self.connection.close()
except Exception:
pass
finally:
self.connection = None
self.interactive_text.append_info("[INFO] Соединение закрыто.")
self.update_status_bar()
else:
messagebox.showinfo("Информация", "Соединение не установлено.")
def start_port_monitoring(self):
"""Запуск мониторинга состояния COM-порта"""
if not self.port_monitor_thread or not self.port_monitor_thread.is_alive():
self.port_monitoring = True
self.port_monitor_thread = threading.Thread(target=self.monitor_port_state, daemon=True)
self.port_monitor_thread.start()
def stop_port_monitoring(self):
"""Остановка мониторинга состояния COM-порта"""
self.port_monitoring = False
if self.port_monitor_thread and self.port_monitor_thread.is_alive():
self.port_monitor_thread.join(timeout=1.0)
def monitor_port_state(self):
"""Мониторинг состояния COM-порта"""
while self.port_monitoring and self.connection:
try:
# Проверяем, что порт всё ещё открыт и отвечает
if not self.connection.is_open:
self.connection.open()
# Если порт отвечает, обновляем статус
self.after(0, self.update_connection_indicator, True)
except (SerialException, OSError):
# Если возникла ошибка, помечаем порт как отключенный
self.after(0, self.update_connection_indicator, False)
# Пытаемся переподключиться
try:
if self.connection:
self.connection.close()
except:
pass
self.connection = None
break
time.sleep(1) # Проверяем состояние каждую секунду
def update_connection_indicator(self, is_connected):
"""Обновление индикатора подключения COM-порта"""
if is_connected:
self.connection_indicator.configure(text="", foreground='green')
else:
self.connection_indicator.configure(text="", foreground='red')
# Отправка команды
def send_command(self):
if not self.connection:
messagebox.showerror("Ошибка", "Сначала установите соединение!")
return
cmd = self.command_entry.get().strip()
if not cmd:
return
self.interactive_text.append_command(f"[CMD] {cmd}")
self.command_entry.delete(0, END) # Очищаем поле ввода
threading.Thread(target=self.process_command, args=(cmd,), daemon=True).start()
# Обработка команды
def process_command(self, cmd):
try:
success, response = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_interactive_text,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
except SerialException as e:
self.interactive_text.append_error(f"[ERROR] Ошибка при отправке команды: {e}")
logging.error(f"Ошибка отправки команды: {e}", exc_info=True)
except Exception as e:
self.interactive_text.append_error(f"[ERROR] Неизвестная ошибка: {e}")
logging.error(f"Неизвестная ошибка: {e}", exc_info=True)
# Добавление текста в текстовое поле
def append_interactive_text(self, text):
if "[ERROR]" in text:
self.interactive_text.append_error(text)
elif "[WARNING]" in text or "[WARN]" in text:
self.interactive_text.append_warning(text)
elif "[INFO]" in text:
self.interactive_text.append_info(text)
else:
self.interactive_text.append_text(text)
# Создание вкладки "Выполнить команды из файла"
def create_file_exec_tab(self, frame):
file_frame = ttk.Frame(frame)
file_frame.pack(fill=X, pady=5)
ttk.Label(file_frame, text="Файл конфигурации:").pack(side=LEFT, padx=5)
self.file_exec_var = StringVar(value=self.settings.get("config_file") or "")
self.file_exec_entry = CustomEntry(file_frame, textvariable=self.file_exec_var, width=40)
self.file_exec_entry.pack(side=LEFT, padx=5)
ttk.Button(file_frame, text="Выбрать", command=self.select_config_file_fileexec).pack(side=LEFT, padx=5)
# Создаем фрейм для кнопок управления
control_frame = ttk.Frame(frame)
control_frame.pack(pady=5)
# Кнопки управления выполнением
self.start_button = ttk.Button(control_frame, text="▶ Старт", command=self.start_execution)
self.start_button.pack(side=LEFT, padx=5)
self.pause_button = ttk.Button(control_frame, text="⏸ Пауза", command=self.pause_execution, state="disabled")
self.pause_button.pack(side=LEFT, padx=5)
self.stop_button = ttk.Button(control_frame, text="⏹ Остановить", command=self.stop_execution, state="disabled")
self.stop_button.pack(side=LEFT, padx=5)
# Кнопка очистки терминала
self.clear_button = ttk.Button(control_frame, text="🗑 Очистить", command=lambda: self.file_exec_text.clear())
self.clear_button.pack(side=LEFT, padx=5)
# Создаем фрейм для индикатора прогресса и таймера
progress_frame = ttk.Frame(frame)
progress_frame.pack(fill=X, pady=5, padx=5)
# Добавляем индикатор прогресса
progress_label_frame = ttk.Frame(progress_frame)
progress_label_frame.pack(fill=X)
self.progress_label = ttk.Label(progress_label_frame, text="Прогресс: 0/0 команд")
self.progress_label.pack(side=LEFT, padx=5)
self.timer_label = ttk.Label(progress_label_frame, text="Время: 00:00:00")
self.timer_label.pack(side=RIGHT, padx=5)
self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate')
self.progress_bar.pack(fill=X, pady=5)
# Используем новый TerminalWidget вместо CustomText
self.file_exec_text = TerminalWidget(frame, height=15)
self.file_exec_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
def append_file_exec_text(self, text):
if "[ERROR]" in text:
self.file_exec_text.append_error(text)
elif "[WARNING]" in text or "[WARN]" in text:
self.file_exec_text.append_warning(text)
elif "[INFO]" in text:
self.file_exec_text.append_info(text)
elif "[CMD]" in text: # Добавляем обработку команд
self.file_exec_text.append_command(text)
else:
self.file_exec_text.append_text(text)
# Выбор файла конфигурации для выполнения команд
def select_config_file_fileexec(self):
def on_config_selected(path):
self.file_exec_var.set(path)
# Создаем и показываем окно выбора конфигурации
config_selector = ConfigSelectorWindow(self, on_config_selected)
self.wait_window(config_selector)
# Выполнение команд из файла
def execute_file_commands(self):
if not self.settings.get("port"):
messagebox.showerror("Ошибка", "COM-порт не выбран!")
return
if not self.file_exec_var.get():
messagebox.showerror("Ошибка", "Файл конфигурации не выбран!")
return
if not self.connection:
self.connection = create_connection(self.settings)
if self.connection:
# Запускаем мониторинг при новом подключении
self.start_port_monitoring()
if not self.connection:
self.append_file_exec_text("[ERROR] Не удалось установить соединение.\n")
return
try:
with open(self.file_exec_var.get(), "r", encoding="utf-8") as file:
self.commands = [line.strip() for line in file if line.strip()]
self.current_command_index = 0
self.execution_paused = False
self.execution_stop = False
# Инициализируем прогресс бар
self.progress_bar['value'] = 0
self.update_progress()
# Запускаем таймер
self.start_time = time.time()
self.elapsed_time = 0
self.timer_running = True
self.update_timer()
# Запускаем выполнение команд в отдельном потоке
threading.Thread(target=self.command_execution_thread, daemon=True).start()
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при чтении файла: {str(e)}\n")
self.reset_execution_buttons()
def command_execution_thread(self):
"""Отдельный поток для выполнения команд"""
copy_mode = self.settings.get("copy_mode", "line")
block_size = self.settings.get("block_size", 15)
# Счетчик последовательных ошибок
consecutive_errors = 0
MAX_CONSECUTIVE_ERRORS = 3 # Максимальное количество последовательных ошибок
def check_connection():
"""Проверка состояния соединения"""
# Если выполнение остановлено пользователем, просто возвращаем False без сообщений
if self.execution_stop:
return False
if not self.connection or not self.connection.is_open:
# Если остановка произошла во время проверки, не показываем сообщение
if self.execution_stop:
return False
self.append_file_exec_text("[ERROR] Соединение потеряно!\n")
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение только если это не ручная остановка
if not self.execution_stop:
self.after(0, lambda: messagebox.showerror(
"Ошибка соединения",
"Соединение с устройством потеряно!\nВыполнение команд приостановлено.\n\n"
"Пожалуйста:\n"
"1. Проверьте подключение\n"
"2. Нажмите 'Продолжить' после восстановления соединения\n"
" или 'Остановить' для прекращения выполнения"
))
return False
return True
def handle_no_response(cmd_or_block, is_block=False):
"""Обработка отсутствия ответа от устройства"""
nonlocal consecutive_errors
consecutive_errors += 1
if consecutive_errors >= MAX_CONSECUTIVE_ERRORS:
self.append_file_exec_text(
f"[ERROR] Обнаружено {consecutive_errors} последовательных ошибок!\n"
"Возможно, устройство не отвечает или проблемы с соединением.\n"
)
# Автоматически ставим на паузу
self.execution_paused = True
self.after(0, lambda: self.pause_button.config(text="▶ Продолжить"))
# Показываем сообщение пользователю
self.after(0, lambda: messagebox.showerror(
"Устройство не отвечает",
f"Обнаружено {consecutive_errors} последовательных ошибок!\n\n"
"Возможные причины:\n"
"1. Устройство не отвечает на команды\n"
"2. Проблемы с соединением\n"
"3. Неверный формат команд\n\n"
"Выполнение приостановлено.\n"
"Проверьте подключение и состояние устройства,\n"
"затем нажмите 'Продолжить' или 'Остановить'."
))
return False
return True
if copy_mode == "line":
# Построчный режим
while self.current_command_index < len(self.commands):
if self.execution_stop:
break
if self.execution_paused:
time.sleep(0.1)
continue
# Проверяем соединение перед каждой командой
if not check_connection():
continue
cmd = self.commands[self.current_command_index]
try:
success, response = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
# Проверяем соединение после неудачной попытки
if not check_connection():
continue
# Обрабатываем отсутствие ответа
if not handle_no_response(cmd):
continue
else:
# Сбрасываем счетчик ошибок при успешном выполнении
consecutive_errors = 0
self.current_command_index += 1
self.after(0, self.update_progress)
time.sleep(1) # Задержка между командами
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении команды: {str(e)}\n")
if not check_connection():
continue
break
else:
# Блочный режим
blocks = generate_command_blocks(self.commands, block_size)
total_blocks = len(blocks)
current_block = 0
while current_block < total_blocks:
if self.execution_stop:
break
if self.execution_paused:
time.sleep(0.1)
continue
# Проверяем соединение перед каждым блоком
if not check_connection():
continue
block = blocks[current_block]
try:
# Выводим блок команд без [CMD] префикса
self.append_file_exec_text(f"Выполнение блока команд:\n{block}\n")
success, response = send_command_and_process_response(
self.connection,
block,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=None, # Отключаем вывод для первой попытки
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success or (response and '^' in response):
self.append_file_exec_text("[WARNING] Ошибка при выполнении блока команд. Отправляю команды по отдельности...\n")
# Проверяем соединение перед отправкой отдельных команд
if not check_connection():
continue
# Обрабатываем отсутствие ответа для блока
if not success and not handle_no_response(block, True):
continue
# Отправляем команды блока по отдельности
for cmd in block.splitlines():
if self.execution_stop:
break
if cmd.strip():
if not check_connection():
break
success, resp = send_command_and_process_response(
self.connection,
cmd,
self.settings.get("timeout", 10),
max_attempts=3,
log_callback=self.append_file_exec_text,
login=self.settings.get("login"),
password=self.settings.get("password"),
is_gui=True
)
if not success:
self.append_file_exec_text(f"[ERROR] Не удалось выполнить команду: {cmd}\n")
if not check_connection():
break
# Обрабатываем отсутствие ответа для отдельной команды
if not handle_no_response(cmd):
break
else:
# Сбрасываем счетчик ошибок при успешном выполнении
consecutive_errors = 0
else:
# Если блок выполнился успешно, выводим ответ и сбрасываем счетчик ошибок
consecutive_errors = 0
if response:
self.append_file_exec_text(f"Ответ устройства:\n{response}\n")
# Обновляем прогресс на основе количества выполненных блоков
current_block += 1
self.current_command_index = (current_block * 100) // total_blocks
self.after(0, self.update_progress)
time.sleep(1)
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при выполнении блока команд: {str(e)}\n")
if not check_connection():
continue
break
# Завершение выполнения
self.after(0, self.execution_completed)
def execution_completed(self):
"""Обработка завершения выполнения в главном потоке"""
if self.execution_stop:
self.append_file_exec_text("[INFO] Выполнение остановлено.\n")
else:
self.append_file_exec_text("[INFO] Выполнение завершено.\n")
self.reset_execution_buttons()
def start_execution(self):
# Проверяем подключение перед стартом
if not self.connection or not self.connection.is_open:
self.connection = create_connection(self.settings)
if self.connection:
# Запускаем мониторинг при новом подключении
self.start_port_monitoring()
else:
self.append_file_exec_text("[ERROR] Не удалось установить соединение.\n")
return
if not hasattr(self, 'commands') or not self.commands:
self.execution_thread = threading.Thread(target=self.execute_file_commands, daemon=True)
self.execution_thread.start()
else:
# Если команды уже загружены, просто возобновляем выполнение
self.execution_paused = False
self.execution_stop = False
self.timer_running = True
threading.Thread(target=self.command_execution_thread, daemon=True).start()
self.start_button.config(state="disabled")
self.pause_button.config(state="normal")
self.stop_button.config(state="normal")
self.update_status_bar() # Обновляем статус бар после подключения
def pause_execution(self):
if not self.execution_paused:
self.execution_paused = True
self.pause_button.config(text="▶ Продолжить")
self.append_file_exec_text("[INFO] Выполнение приостановлено.\n")
else:
self.execution_paused = False
self.pause_button.config(text="⏸ Пауза")
self.append_file_exec_text("[INFO] Выполнение возобновлено.\n")
def stop_execution(self):
self.execution_stop = True
self.execution_paused = False
self.timer_running = False
# Отключаемся от COM-порта
if self.connection:
try:
self.stop_port_monitoring()
self.connection.close()
self.connection = None
self.append_file_exec_text("[INFO] Соединение закрыто.\n")
except Exception as e:
self.append_file_exec_text(f"[ERROR] Ошибка при закрытии соединения: {str(e)}\n")
self.current_command_index = 0 # Сбрасываем индекс текущей команды
self.progress_bar['value'] = 0 # Сбрасываем прогресс-бар
self.update_progress() # Обновляем отображение прогресса
self.append_file_exec_text("[INFO] Выполнение остановлено.\n")
self.reset_execution_buttons() # Сбрасываем состояние кнопок
self.update_status_bar() # Обновляем статус бар
def reset_execution_buttons(self):
self.start_button.config(state="normal")
self.pause_button.config(state="disabled", text="⏸ Пауза")
self.stop_button.config(state="disabled")
self.execution_paused = False
self.execution_stop = False
self.timer_running = False
# Очищаем список команд, чтобы при следующем старте они загрузились заново
if hasattr(self, 'commands'):
delattr(self, 'commands')
def update_timer(self):
"""Обновление таймера в главном потоке"""
if self.timer_running:
if not self.execution_paused:
self.elapsed_time = time.time() - self.start_time
hours = int(self.elapsed_time // 3600)
minutes = int((self.elapsed_time % 3600) // 60)
seconds = int(self.elapsed_time % 60)
self.timer_label.config(text=f"Время: {hours:02d}:{minutes:02d}:{seconds:02d}")
self.after(1000, self.update_timer) # Обновление каждую секунду
def update_progress(self):
"""Обновление прогресса в главном потоке"""
if hasattr(self, 'commands'):
total_commands = len(self.commands)
current = self.current_command_index
self.progress_label.config(text=f"Прогресс: {current}/{total_commands} команд")
if total_commands > 0:
progress = (current / total_commands) * 100
self.progress_bar['value'] = progress
# Создание вкладки "Редактор конфигурационного файла"
def create_config_editor_tab(self, frame):
top_frame = ttk.Frame(frame)
top_frame.pack(fill=X, pady=5)
ttk.Label(top_frame, text="Файл конфигурации:").pack(side=LEFT, padx=5)
self.editor_file_var = StringVar(value=self.settings.get("config_file") or "")
CustomEntry(top_frame, textvariable=self.editor_file_var, width=40).pack(side=LEFT, padx=5)
ttk.Button(top_frame, text="Выбрать", command=self.select_config_file_editor).pack(side=LEFT, padx=5)
ttk.Button(top_frame, text="Загрузить", command=self.load_config_file).pack(side=LEFT, padx=5)
ttk.Button(top_frame, text="Сохранить", command=self.save_config_file).pack(side=LEFT, padx=5)
self.config_editor_text = CustomText(frame, wrap="word")
self.config_editor_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Выбор файла конфигурации для редактирования
def select_config_file_editor(self):
select_config_file(self, self.editor_file_var, save_to_settings=True)
# Загрузка файла конфигурации
def load_config_file(self):
filename = self.editor_file_var.get()
if not filename or not os.path.exists(filename):
messagebox.showerror("Ошибка", "Файл конфигурации не выбран или не существует.")
return
try:
with open(filename, "r", encoding="utf-8") as f:
content = f.read()
self.config_editor_text.delete("1.0", END)
self.config_editor_text.insert(END, content)
messagebox.showinfo("Информация", "Файл загружен.")
except Exception as e:
logging.error(f"Ошибка загрузки файла: {e}", exc_info=True)
messagebox.showerror("Ошибка", f"Не удалось загрузить файл:\n{e}")
# Сохранение файла конфигурации
def save_config_file(self):
filename = self.editor_file_var.get()
if not filename:
messagebox.showerror("Ошибка", "Файл конфигурации не выбран.")
return
try:
content = self.config_editor_text.get("1.0", END)
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
messagebox.showinfo("Информация", "Файл сохранён.")
except Exception as e:
logging.error(f"Ошибка сохранения файла: {e}", exc_info=True)
messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{e}")
# Открытие окна "О программе"
def open_about(self):
about_window = AboutWindow(self)
about_window.transient(self)
about_window.grab_set()
# Создание вкладки TFTP сервера
def create_tftp_tab(self, frame):
# Создаем фрейм для управления TFTP сервером
tftp_frame = ttk.Frame(frame)
tftp_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Создаем и размещаем элементы управления
controls_frame = ttk.LabelFrame(tftp_frame, text="Управление TFTP сервером")
controls_frame.pack(fill=X, padx=5, pady=5)
# IP адрес
ip_frame = ttk.Frame(controls_frame)
ip_frame.pack(fill=X, padx=5, pady=2)
ttk.Label(ip_frame, text="IP адрес:").pack(side=LEFT, padx=5)
self.tftp_ip_var = StringVar(value="0.0.0.0")
self.tftp_ip_combo = ttk.Combobox(ip_frame, textvariable=self.tftp_ip_var, state="readonly")
self.tftp_ip_combo.pack(side=LEFT, fill=X, expand=True, padx=5)
ttk.Button(ip_frame, text="Обновить", command=self.update_network_adapters).pack(side=LEFT, padx=5)
# Заполняем список адаптеров
self.update_network_adapters()
# Порт
port_frame = ttk.Frame(controls_frame)
port_frame.pack(fill=X, padx=5, pady=2)
ttk.Label(port_frame, text="Порт:").pack(side=LEFT, padx=5)
self.tftp_port_var = StringVar(value="69")
self.tftp_port_entry = CustomEntry(port_frame, textvariable=self.tftp_port_var)
self.tftp_port_entry.pack(fill=X, expand=True, padx=5)
# Кнопки управления
buttons_frame = ttk.Frame(controls_frame)
buttons_frame.pack(fill=X, padx=5, pady=5)
self.start_tftp_button = ttk.Button(
buttons_frame,
text="Запустить сервер",
command=self.start_tftp_server
)
self.start_tftp_button.pack(side=LEFT, padx=5)
self.stop_tftp_button = ttk.Button(
buttons_frame,
text="Остановить сервер",
command=self.stop_tftp_server,
state="disabled"
)
self.stop_tftp_button.pack(side=LEFT, padx=5)
# Кнопка очистки лога
self.clear_tftp_button = ttk.Button(
buttons_frame,
text="🗑 Очистить лог",
command=lambda: self.tftp_log_text.clear()
)
self.clear_tftp_button.pack(side=LEFT, padx=5)
# Лог сервера
log_frame = ttk.LabelFrame(tftp_frame, text="Лог сервера")
log_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Используем новый TerminalWidget вместо CustomText
self.tftp_log_text = TerminalWidget(log_frame, height=10)
self.tftp_log_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Статус передач
transfers_frame = ttk.LabelFrame(tftp_frame, text="Активные передачи")
transfers_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Создаем таблицу для отображения активных передач
columns = ("client", "filename", "progress", "remaining", "time")
self.transfers_tree = ttk.Treeview(transfers_frame, columns=columns, show="headings")
# Настраиваем заголовки колонок
self.transfers_tree.heading("client", text="Клиент")
self.transfers_tree.heading("filename", text="Файл")
self.transfers_tree.heading("progress", text="Прогресс")
self.transfers_tree.heading("remaining", text="Осталось")
self.transfers_tree.heading("time", text="Время")
# Настраиваем ширину колонок
self.transfers_tree.column("client", width=120)
self.transfers_tree.column("filename", width=150)
self.transfers_tree.column("progress", width=100)
self.transfers_tree.column("remaining", width=100)
self.transfers_tree.column("time", width=80)
self.transfers_tree.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Инициализация TFTP сервера
self.tftp_server = None
self.tftp_server_thread = None
# Запуск TFTP сервера
def start_tftp_server(self):
try:
# Получаем выбранный IP-адрес
ip = self.tftp_ip_var.get()
if not ip:
messagebox.showerror("Ошибка", "Выберите IP-адрес для TFTP сервера")
return
# Проверяем корректность порта
try:
port = int(self.tftp_port_var.get())
if port <= 0 or port > 65535:
raise ValueError("Порт должен быть в диапазоне 1-65535")
except ValueError as e:
messagebox.showerror("Ошибка", f"Некорректный порт: {str(e)}")
return
# Создаем экземпляр TFTP сервера
self.tftp_server = TFTPServer("Firmware")
# Устанавливаем callback для логирования
def log_callback(message):
# Фильтруем дублирующиеся сообщения о запуске/остановке сервера
if "[INFO] TFTP сервер запущен" in message and hasattr(self, '_server_started'):
return
if "[INFO] TFTP сервер остановлен" in message and hasattr(self, '_server_stopped'):
return
self.append_tftp_log(message)
# Устанавливаем флаги для отслеживания состояния
if "[INFO] TFTP сервер запущен" in message:
self._server_started = True
elif "[INFO] TFTP сервер остановлен" in message:
self._server_stopped = True
# Обновляем информацию о передачах
self.update_transfers_info()
self.tftp_server.set_log_callback(log_callback)
# Запускаем сервер в отдельном потоке
self.tftp_server_thread = threading.Thread(
target=self.run_tftp_server,
args=(ip, port),
daemon=True
)
self.tftp_server_thread.start()
# Обновляем состояние кнопок и элементов управления
self.start_tftp_button.config(state="disabled")
self.stop_tftp_button.config(state="normal")
self.tftp_ip_combo.config(state="disabled")
self.tftp_port_entry.config(state="disabled")
# Запускаем периодическое обновление информации о передачах
self.update_transfers_periodically()
# Обновляем статус бар после запуска сервера
self.update_status_bar()
except Exception as e:
self.append_tftp_log(f"[ERROR] Ошибка запуска сервера: {str(e)}")
messagebox.showerror("Ошибка", f"Не удалось запустить TFTP сервер: {str(e)}")
# Запуск TFTP сервера в отдельном потоке
def run_tftp_server(self, ip, port):
try:
self.tftp_server.start_server(ip, port)
except Exception as e:
self.append_tftp_log(f"[ERROR] Ошибка работы сервера: {str(e)}")
# Остановка TFTP сервера
def stop_tftp_server(self):
if self.tftp_server:
try:
# Отключаем кнопки на время остановки сервера
self.start_tftp_button.config(state="disabled")
self.stop_tftp_button.config(state="disabled")
# Сбрасываем флаги состояния
if hasattr(self, '_server_started'):
delattr(self, '_server_started')
if hasattr(self, '_server_stopped'):
delattr(self, '_server_stopped')
# Останавливаем сервер
self.tftp_server.stop_server()
# Ждем завершения потока сервера с таймаутом
if self.tftp_server_thread:
self.tftp_server_thread.join(timeout=5.0)
if self.tftp_server_thread.is_alive():
self.append_tftp_log("[WARN] Превышено время ожидания остановки сервера")
# Очищаем ссылки на сервер и поток
self.tftp_server = None
self.tftp_server_thread = None
# Обновляем состояние кнопок
self.start_tftp_button.config(state="normal")
self.stop_tftp_button.config(state="disabled")
self.tftp_ip_combo.config(state="normal")
self.tftp_port_entry.config(state="normal")
# Очищаем таблицу передач
for item in self.transfers_tree.get_children():
self.transfers_tree.delete(item)
# Обновляем статус бар после остановки сервера
self.update_status_bar()
except Exception as e:
self.append_tftp_log(f"[ERROR] Ошибка остановки сервера: {str(e)}")
messagebox.showerror("Ошибка", f"Не удалось остановить TFTP сервер: {str(e)}")
# Восстанавливаем состояние кнопок в случае ошибки
self.start_tftp_button.config(state="disabled")
self.stop_tftp_button.config(state="normal")
def append_tftp_log(self, text):
if "[ERROR]" in text:
self.tftp_log_text.append_error(text)
elif "[WARNING]" in text or "[WARN]" in text:
self.tftp_log_text.append_warning(text)
elif "[INFO]" in text:
self.tftp_log_text.append_info(text)
else:
self.tftp_log_text.append_text(text)
# Обновление информации об активных передачах
def update_transfers_info(self):
if not self.tftp_server:
return
# Очищаем текущие записи
for item in self.transfers_tree.get_children():
self.transfers_tree.delete(item)
# Добавляем информацию о текущих передачах
for client_addr, transfer_info in self.tftp_server.active_transfers.items():
filename = transfer_info['filename']
bytes_sent = transfer_info['bytes_sent']
filesize = transfer_info['filesize']
start_time = transfer_info['start_time']
# Вычисляем прогресс
progress = f"{bytes_sent}/{filesize} байт"
remaining_bytes = filesize - bytes_sent
elapsed_time = time.time() - start_time
# Вычисляем скорость передачи (байт/сек)
if elapsed_time > 0:
transfer_speed = bytes_sent / elapsed_time
# Вычисляем оставшееся время
if transfer_speed > 0:
remaining_time = remaining_bytes / transfer_speed
remaining_str = f"{remaining_bytes} байт (~{int(remaining_time)}с)"
else:
remaining_str = f"{remaining_bytes} байт (неизвестно)"
else:
remaining_str = f"{remaining_bytes} байт (вычисляется...)"
# Добавляем запись в таблицу
self.transfers_tree.insert("", END, values=(
f"{client_addr[0]}:{client_addr[1]}",
filename,
progress,
remaining_str,
f"{elapsed_time:.1f}с"
))
# Периодическое обновление информации о передачах
def update_transfers_periodically(self):
if self.tftp_server and self.tftp_server.running:
self.update_transfers_info()
# Планируем следующее обновление через 1 секунду
self.after(1000, self.update_transfers_periodically)
# Обновление списка сетевых адаптеров
def update_network_adapters(self):
adapters = get_network_adapters()
self.tftp_ip_combo["values"] = adapters
if not self.tftp_ip_var.get() in adapters:
self.tftp_ip_var.set(adapters[0])
# Добавляем новый метод для создания статус бара
def create_status_bar(self):
self.status_bar = ttk.Frame(self)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# Создаем фрейм для индикаторов
indicators_frame = ttk.Frame(self.status_bar)
indicators_frame.pack(side=tk.LEFT)
# Индикатор подключения к коммутатору
self.connection_indicator_frame = ttk.Frame(indicators_frame)
self.connection_indicator_frame.pack(side=tk.LEFT)
ttk.Label(self.connection_indicator_frame, text="COM:", padding=(5, 2)).pack(side=tk.LEFT)
self.connection_indicator = ttk.Label(
self.connection_indicator_frame,
text="",
font=("Segoe UI", 10),
width=2,
anchor='center',
padding=(2, 2)
)
self.connection_indicator.pack(side=tk.LEFT)
# Индикатор TFTP сервера
self.tftp_indicator_frame = ttk.Frame(indicators_frame)
self.tftp_indicator_frame.pack(side=tk.LEFT, padx=10)
ttk.Label(self.tftp_indicator_frame, text="TFTP:", padding=(5, 2)).pack(side=tk.LEFT)
self.tftp_indicator = ttk.Label(
self.tftp_indicator_frame,
text="",
font=("Segoe UI", 10),
width=2,
anchor='center',
padding=(2, 2)
)
self.tftp_indicator.pack(side=tk.LEFT)
ttk.Separator(self.status_bar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
# Остальные элементы статус бара...
self.port_label = ttk.Label(self.status_bar, text="", padding=(5, 2))
self.port_label.pack(side=tk.LEFT)
ttk.Separator(self.status_bar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
self.baudrate_label = ttk.Label(self.status_bar, text="", padding=(5, 2))
self.baudrate_label.pack(side=tk.LEFT)
ttk.Separator(self.status_bar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
self.copy_mode_label = ttk.Label(self.status_bar, text="", padding=(5, 2))
self.copy_mode_label.pack(side=tk.LEFT)
ttk.Separator(self.status_bar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
self.version_label = ttk.Label(self.status_bar, text=f"Версия: {VERSION}", padding=(5, 2))
self.version_label.pack(side=tk.RIGHT)
# Обновляем метод update_status_bar
def update_status_bar(self):
"""Обновление статус-бара"""
# Проверяем реальное состояние подключения
is_connected = bool(self.connection and self.connection.is_open)
self.update_connection_indicator(is_connected)
# Обновляем индикатор TFTP сервера
if self.tftp_server and self.tftp_server.running:
self.tftp_indicator.configure(text="", foreground='green')
else:
self.tftp_indicator.configure(text="", foreground='red')
# Остальные обновления статус бара
port = self.settings.get("port", "Не выбран")
baudrate = self.settings.get("baudrate", "9600")
copy_mode = "Построчный" if self.settings.get("copy_mode") == "line" else "Блочный"
self.port_label.config(text=f"Порт: {port}")
self.baudrate_label.config(text=f"Скорость: {baudrate}")
self.copy_mode_label.config(text=f"Режим: {copy_mode}")
def on_file_exec_drop(self, event):
"""Обработка drop файла во вкладке выполнения команд"""
try:
files = event.data.split()
if files:
file = files[0] # Берем только первый файл
if file.lower().endswith('.txt'):
filename = os.path.basename(file)
new_path = os.path.join("Configs", filename)
if os.path.exists(new_path):
if not messagebox.askyesno(
"Подтверждение",
f"Файл {filename} уже существует. Перезаписать?"
):
return
# Копируем файл в папку Configs
with open(file, 'r', encoding='utf-8') as source:
content = source.read()
with open(new_path, 'w', encoding='utf-8') as dest:
dest.write(content)
# Устанавливаем путь к файлу
self.file_exec_var.set(new_path)
messagebox.showinfo("Успех", "Файл конфигурации успешно добавлен")
else:
messagebox.showerror("Ошибка", "Поддерживаются только текстовые файлы (.txt)")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось добавить файл: {str(e)}")
# Класс для окна выбора конфигурации
class ConfigSelectorWindow(tk.Toplevel):
def __init__(self, parent, callback):
super().__init__(parent)
self.title("Выбор конфигурации")
self.geometry("600x400")
self.callback = callback
# Создаем основной фрейм
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=BOTH, expand=True)
# Создаем фрейм для кнопок управления
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=X, pady=5)
# Кнопка "Добавить новую конфигурацию"
add_button = ttk.Button(
button_frame,
text=" Добавить новую конфигурацию",
command=self.add_new_config
)
add_button.pack(side=LEFT, padx=5)
# Кнопка "Создать папку"
create_folder_button = ttk.Button(
button_frame,
text="📁 Создать папку",
command=self.create_new_folder
)
create_folder_button.pack(side=LEFT, padx=5)
# Создаем фрейм для дерева и скроллбара
tree_frame = ttk.Frame(main_frame)
tree_frame.pack(fill=BOTH, expand=True, pady=5)
# Создаем Treeview для отображения файлов и папок
self.tree = ttk.Treeview(tree_frame, selectmode="browse")
# Добавляем скроллбар
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.tree.pack(side=LEFT, fill=BOTH, expand=True)
self.tree.configure(yscrollcommand=scrollbar.set)
# Настраиваем колонки
self.tree["columns"] = () # Убираем колонку действий
self.tree.column("#0", width=400, stretch=True)
self.tree.heading("#0", text="Конфигурация")
# Создаем контекстное меню
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="Редактировать", command=self.edit_selected)
self.context_menu.add_command(label="Переименовать", command=self.rename_selected)
self.context_menu.add_separator()
self.context_menu.add_command(label="Удалить", command=self.delete_selected)
# Привязываем события
self.tree.bind("<Double-1>", self.on_double_click)
self.tree.bind("<Button-3>", self.show_context_menu)
# Загружаем файлы и папки
self.load_configs()
# Центрируем окно
self.center_window()
# Делаем окно модальным
self.transient(parent)
self.grab_set()
def create_new_folder(self):
"""Создание новой папки"""
selected_item = self.get_selected_item()
parent_path = "Configs"
if selected_item:
item_text = self.tree.item(selected_item)['text']
if item_text.startswith("📁"):
parent_path = self.get_full_path(selected_item)
else:
parent_item = self.tree.parent(selected_item)
if parent_item:
parent_path = self.get_full_path(parent_item)
folder_name = simpledialog.askstring(
"Создать папку",
"Введите имя новой папки:"
)
if folder_name:
try:
new_folder_path = os.path.join(parent_path, folder_name)
os.makedirs(new_folder_path, exist_ok=True)
self.load_configs()
messagebox.showinfo("Успех", "Папка успешно создана")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось создать папку: {str(e)}")
def show_context_menu(self, event):
"""Показ контекстного меню"""
item = self.tree.identify('item', event.x, event.y)
if item:
self.tree.selection_set(item)
if self.tree.item(item)['text'].startswith("📄"): # Только для файлов
self.context_menu.post(event.x_root, event.y_root)
def get_selected_item(self):
"""Получение выбранного элемента"""
selection = self.tree.selection()
if selection:
return selection[0]
return None
def edit_selected(self):
"""Редактирование выбранного файла"""
item_id = self.get_selected_item()
if item_id:
path = self.get_full_path(item_id)
self.edit_config(path)
def rename_selected(self):
"""Переименование выбранного файла"""
item_id = self.get_selected_item()
if item_id:
path = self.get_full_path(item_id)
self.rename_config(item_id, path)
def delete_selected(self):
"""Удаление выбранного файла"""
item_id = self.get_selected_item()
if item_id:
path = self.get_full_path(item_id)
self.delete_config(path)
def load_configs(self):
"""Загрузка конфигураций из папки Configs"""
# Очищаем дерево
for item in self.tree.get_children():
self.tree.delete(item)
# Функция для рекурсивного добавления файлов и папок
def add_directory(path, parent=""):
try:
# Сортируем содержимое: сначала папки, потом файлы
items = os.listdir(path)
directories = []
files = []
for item in items:
full_path = os.path.join(path, item)
if os.path.isdir(full_path):
directories.append(item)
elif item.endswith('.txt'):
files.append(item)
# Добавляем папки
for directory in sorted(directories):
full_path = os.path.join(path, directory)
folder_id = self.tree.insert(
parent,
"end",
text="📁 " + directory,
open=False
)
add_directory(full_path, folder_id)
# Добавляем файлы
for file in sorted(files):
self.tree.insert(
parent,
"end",
text="📄 " + file
)
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка при загрузке файлов: {str(e)}")
# Загружаем файлы, начиная с корневой папки Configs
add_directory("Configs")
def add_new_config(self):
"""Добавление новой конфигурации через проводник Windows"""
file_path = filedialog.askopenfilename(
title="Выберите файл конфигурации",
filetypes=[("Текстовые файлы", "*.txt")],
initialdir=os.path.abspath(".")
)
if not file_path: # Если пользователь отменил выбор файла
return
try:
# Создаем имя файла в папке Configs
filename = os.path.basename(file_path)
new_path = os.path.join("Configs", filename)
# Если файл уже существует, спрашиваем о перезаписи
if os.path.exists(new_path):
if not messagebox.askyesno(
"Подтверждение",
f"Файл {filename} уже существует. Перезаписать?"
):
return
# Копируем файл
with open(file_path, 'r', encoding='utf-8') as source:
content = source.read()
with open(new_path, 'w', encoding='utf-8') as dest:
dest.write(content)
self.load_configs() # Обновляем список
messagebox.showinfo("Успех", "Файл конфигурации успешно добавлен")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось добавить файл: {str(e)}")
def get_full_path(self, item_id):
"""Получение полного пути к файлу/папке"""
path_parts = []
while item_id:
text = self.tree.item(item_id)['text']
# Удаляем эмодзи из начала текста
if text.startswith("📁 "):
text = text[2:].strip()
elif text.startswith("📄 "):
text = text[2:].strip()
path_parts.insert(0, text)
item_id = self.tree.parent(item_id)
return os.path.join("Configs", *path_parts)
def select_config(self, path):
"""Выбор конфигурации"""
if os.path.isfile(path):
self.callback(path)
self.destroy()
def edit_config(self, path):
"""Редактирование конфигурации"""
if os.path.isfile(path):
# Переключаемся на вкладку редактора и загружаем файл
self.master.notebook.select(2) # Индекс вкладки редактора
self.master.editor_file_var.set(path)
self.master.load_config_file()
self.destroy()
def rename_config(self, item_id, old_path):
"""Переименование конфигурации"""
if not os.path.isfile(old_path):
return
# Получаем текущее имя файла без эмодзи
current_name = self.tree.item(item_id)['text']
if current_name.startswith("📄 "):
current_name = current_name[2:].strip()
# Запрашиваем новое имя
new_name = simpledialog.askstring(
"Переименование",
"Введите новое имя файла:",
initialvalue=current_name
)
if new_name:
if not new_name.endswith('.txt'):
new_name += '.txt'
new_path = os.path.join(os.path.dirname(old_path), new_name)
try:
os.rename(old_path, new_path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось переименовать файл: {str(e)}")
def delete_config(self, path):
"""Удаление конфигурации"""
if not os.path.isfile(path):
return
# Запрашиваем подтверждение
if messagebox.askyesno(
"Подтверждение",
f"Вы действительно хотите удалить файл {os.path.basename(path)}?"
):
try:
os.remove(path)
self.load_configs() # Перезагружаем список
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось удалить файл: {str(e)}")
def on_double_click(self, event):
"""Обработка двойного клика"""
item_id = self.tree.identify('item', event.x, event.y)
if not item_id:
return
# Если это папка, разворачиваем/сворачиваем её
if not self.tree.item(item_id)['text'].startswith("📄"): # Если не файл
if self.tree.item(item_id)['open']:
self.tree.item(item_id, open=False)
else:
self.tree.item(item_id, open=True)
else:
# Если это файл, выбираем его
full_path = self.get_full_path(item_id)
self.select_config(full_path)
def center_window(self):
"""Центрирование окна на экране"""
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")
# ==========================
# Основной запуск приложения
# ==========================
def main():
setup_logging()
settings = settings_load()
app = SerialAppGUI(settings)
app.mainloop()
# ==========================
# Основной запуск приложения
# ==========================
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logging.info("Программа прервана пользователем (KeyboardInterrupt).")
sys.exit(0)
except Exception as e:
logging.critical(f"Неизвестная ошибка: {e}", exc_info=True)
sys.exit(1)