Compare commits
21 Commits
dc81fed9d9
...
v1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| d442b790b7 | |||
| 56a8d80de8 | |||
| 57d173e00e | |||
| cb5329ddb7 | |||
| a140b7d8a0 | |||
| 2c9edcd859 | |||
| 5a00efd175 | |||
| 2f4b2985cd | |||
| d1a870fed7 | |||
| 2e2dd9e705 | |||
| d937042ea2 | |||
| 136c7877d3 | |||
| 467d582095 | |||
| 16526b4643 | |||
| b8bae39a17 | |||
| 6d2819a860 | |||
| a252a0f153 | |||
| 3126811f09 | |||
| f1ca31c198 | |||
| c95915483f | |||
| 299ce329f7 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,10 +1,8 @@
|
|||||||
app.log
|
app.log
|
||||||
Settings/settings.json
|
Settings/settings.json
|
||||||
Configs/Eltex MES2424 AC - Сеть FTTB 2G, доп.txt
|
Configs/
|
||||||
Configs/конфиг доп 3750-52 с айпи 172.17.141.133 .txt
|
|
||||||
DALL·E 2024-12-29 01.01.02 - Square vector logo_ A clean and minimalistic app icon for serial port management software. The design prominently features a simplified rectangular CO.ico
|
|
||||||
test.py
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
Firmware/1.jpg
|
Firmware/1.jpg
|
||||||
Firmware/2
|
Firmware/2
|
||||||
output/
|
output/
|
||||||
|
.venv/
|
||||||
922
ComConfigCopy.py
922
ComConfigCopy.py
@@ -1,922 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Это программа для копирования конфигураций на коммутаторы
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# import argparse Использовался для получения аргументов из командной строки (не используется)
|
|
||||||
# import platform Использовался для получения списка сетевых адаптеров (не используется)
|
|
||||||
# import subprocess Использовался для получения списка сетевых адаптеров (не используется)
|
|
||||||
# import socket не используется
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from getpass import getpass
|
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import (
|
|
||||||
StringVar,
|
|
||||||
END,
|
|
||||||
BOTH,
|
|
||||||
LEFT,
|
|
||||||
X,
|
|
||||||
W,
|
|
||||||
filedialog,
|
|
||||||
messagebox,
|
|
||||||
simpledialog,
|
|
||||||
)
|
|
||||||
from tkinter import ttk
|
|
||||||
|
|
||||||
import serial
|
|
||||||
import serial.tools.list_ports
|
|
||||||
from serial.serialutil import SerialException
|
|
||||||
from about_window import AboutWindow
|
|
||||||
# from TFTPServer import TFTPServerThread
|
|
||||||
|
|
||||||
# Создаем необходимые папки
|
|
||||||
os.makedirs("Logs", exist_ok=True)
|
|
||||||
os.makedirs("Configs", exist_ok=True)
|
|
||||||
os.makedirs("Settings", exist_ok=True)
|
|
||||||
# os.makedirs("Firmware", exist_ok=True)
|
|
||||||
|
|
||||||
# Файл настроек находится в папке Settings
|
|
||||||
SETTINGS_FILE = os.path.join("Settings", "settings.json")
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Функции работы с настройками и логированием
|
|
||||||
# ==========================
|
|
||||||
|
|
||||||
def setup_logging():
|
|
||||||
"""Настройка логирования с использованием RotatingFileHandler."""
|
|
||||||
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)
|
|
||||||
|
|
||||||
def settings_load():
|
|
||||||
"""Загрузка настроек из JSON-файла или создание настроек по умолчанию."""
|
|
||||||
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
|
|
||||||
|
|
||||||
def settings_save(settings):
|
|
||||||
"""Сохранение настроек в JSON-файл."""
|
|
||||||
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]
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Функции работы с сетевыми адаптерами (не используются)
|
|
||||||
# ==========================
|
|
||||||
|
|
||||||
# def list_network_adapters():
|
|
||||||
# """Возвращает список названий сетевых адаптеров (Windows)."""
|
|
||||||
# adapters = []
|
|
||||||
# if platform.system() == "Windows":
|
|
||||||
# try:
|
|
||||||
# output = subprocess.check_output(
|
|
||||||
# 'wmic nic get NetConnectionID',
|
|
||||||
# shell=True,
|
|
||||||
# encoding="cp866"
|
|
||||||
# )
|
|
||||||
# for line in output.splitlines():
|
|
||||||
# line = line.strip()
|
|
||||||
# if line and line != "NetConnectionID":
|
|
||||||
# adapters.append(line)
|
|
||||||
# except Exception as e:
|
|
||||||
# logging.error(f"Ошибка при получении списка адаптеров: {e}", exc_info=True)
|
|
||||||
# else:
|
|
||||||
# adapters = ["eth0"]
|
|
||||||
# return adapters
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Функции работы с COM-соединением
|
|
||||||
# ==========================
|
|
||||||
|
|
||||||
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,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Выполнение команд из файла конфигурации.
|
|
||||||
Если передан log_callback, вывод будет отображаться в GUI.
|
|
||||||
Теперь при обнаружении ошибки (например, если в ответе присутствует символ '^')
|
|
||||||
команда будет отправляться повторно.
|
|
||||||
"""
|
|
||||||
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"\nОтправка команды: {cmd} (Попытка {attempt+1} из {max_attempts})\n"
|
|
||||||
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"\nОтправка блока команд:\n{block}\n"
|
|
||||||
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"\nОтправка команды: {cmd} (Попытка {attempt+1} из {max_attempts})\n"
|
|
||||||
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)
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Графический интерфейс (Tkinter)
|
|
||||||
# ==========================
|
|
||||||
|
|
||||||
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).pack(side=LEFT)
|
|
||||||
ttk.Radiobutton(copy_mode_frame, text="Блоками", value="block",
|
|
||||||
variable=self.copy_mode_var).pack(side=LEFT)
|
|
||||||
|
|
||||||
# Размер блока
|
|
||||||
ttk.Label(settings_frame, text="Размер блока:").grid(row=4, column=0, sticky=W, pady=5)
|
|
||||||
self.block_size_var = StringVar(value=str(settings.get("block_size", 15)))
|
|
||||||
block_size_entry = ttk.Entry(settings_frame, textvariable=self.block_size_var)
|
|
||||||
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 = ttk.Entry(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.center_window()
|
|
||||||
|
|
||||||
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("Ошибка", "Проверьте правильность введенных значений")
|
|
||||||
|
|
||||||
class SerialAppGUI(tk.Tk):
|
|
||||||
def __init__(self, settings):
|
|
||||||
super().__init__()
|
|
||||||
self.title("Serial Device Manager")
|
|
||||||
self.geometry("900x700")
|
|
||||||
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.bind_class("Text", "<Control-c>", lambda event: event.widget.event_generate("<<Copy>>"))
|
|
||||||
self.bind_class("Text", "<Control-v>", lambda event: event.widget.event_generate("<<Paste>>"))
|
|
||||||
self.bind_class("Text", "<Control-x>", lambda event: event.widget.event_generate("<<Cut>>"))
|
|
||||||
self.bind_class("Entry", "<Control-c>", lambda event: event.widget.event_generate("<<Copy>>"))
|
|
||||||
self.bind_class("Entry", "<Control-v>", lambda event: event.widget.event_generate("<<Paste>>"))
|
|
||||||
self.bind_class("Entry", "<Control-x>", lambda event: event.widget.event_generate("<<Cut>>"))
|
|
||||||
|
|
||||||
self.create_menu()
|
|
||||||
self.create_tabs()
|
|
||||||
|
|
||||||
# Проверка первого запуска
|
|
||||||
self.check_first_run()
|
|
||||||
|
|
||||||
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_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.open_about)
|
|
||||||
|
|
||||||
def open_settings(self):
|
|
||||||
settings_window = SettingsWindow(self, self.settings, self.on_settings_changed)
|
|
||||||
settings_window.transient(self)
|
|
||||||
settings_window.grab_set()
|
|
||||||
|
|
||||||
def on_settings_changed(self):
|
|
||||||
# Обновляем настройки в основном приложении
|
|
||||||
self.settings = settings_load()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.notebook.add(interactive_frame, text="Интерактивный режим")
|
|
||||||
self.notebook.add(file_exec_frame, text="Выполнение файла")
|
|
||||||
self.notebook.add(config_editor_frame, text="Редактор конфигурации")
|
|
||||||
|
|
||||||
self.create_interactive_tab(interactive_frame)
|
|
||||||
self.create_file_exec_tab(file_exec_frame)
|
|
||||||
self.create_config_editor_tab(config_editor_frame)
|
|
||||||
|
|
||||||
# -------------- Вкладка "Интерактивный режим" --------------
|
|
||||||
def create_interactive_tab(self, frame):
|
|
||||||
control_frame = ttk.Frame(frame)
|
|
||||||
control_frame.pack(fill=X, pady=5)
|
|
||||||
ttk.Button(control_frame, text="Подключиться", command=self.connect_device).pack(side=LEFT, padx=5)
|
|
||||||
ttk.Button(control_frame, text="Отключиться", command=self.disconnect_device).pack(side=LEFT, padx=5)
|
|
||||||
|
|
||||||
self.interactive_text = tk.Text(frame, wrap="word", 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 = ttk.Entry(input_frame, width=50)
|
|
||||||
self.command_entry.pack(side=LEFT, padx=5)
|
|
||||||
ttk.Button(input_frame, text="Отправить", command=self.send_command).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.append_interactive_text("[INFO] Подключение установлено.\n")
|
|
||||||
else:
|
|
||||||
self.append_interactive_text("[ERROR] Не удалось установить соединение.\n")
|
|
||||||
|
|
||||||
def disconnect_device(self):
|
|
||||||
if self.connection:
|
|
||||||
try:
|
|
||||||
self.connection.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.connection = None
|
|
||||||
self.append_interactive_text("[INFO] Соединение закрыто.\n")
|
|
||||||
else:
|
|
||||||
messagebox.showinfo("Информация", "Соединение не установлено.")
|
|
||||||
|
|
||||||
def send_command(self):
|
|
||||||
if not self.connection:
|
|
||||||
messagebox.showerror("Ошибка", "Сначала установите соединение!")
|
|
||||||
return
|
|
||||||
cmd = self.command_entry.get().strip()
|
|
||||||
if not cmd:
|
|
||||||
return
|
|
||||||
self.append_interactive_text(f"[INFO] Отправка команды: {cmd}\n")
|
|
||||||
threading.Thread(target=self.process_command, args=(cmd,), daemon=True).start()
|
|
||||||
|
|
||||||
def process_command(self, cmd):
|
|
||||||
try:
|
|
||||||
max_attempts = 3
|
|
||||||
attempt = 0
|
|
||||||
while attempt < max_attempts:
|
|
||||||
self.connection.write((cmd + "\n").encode())
|
|
||||||
logging.info(f"Отправлена команда: {cmd} (Попытка {attempt+1})")
|
|
||||||
response = read_response(
|
|
||||||
self.connection,
|
|
||||||
self.settings.get("timeout", 10),
|
|
||||||
login=self.settings.get("login"),
|
|
||||||
password=self.settings.get("password"),
|
|
||||||
is_gui=True,
|
|
||||||
)
|
|
||||||
if response:
|
|
||||||
if '^' in response:
|
|
||||||
self.append_interactive_text(
|
|
||||||
f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n"
|
|
||||||
f"Ответ устройства:\n{response}\n"
|
|
||||||
f"Повторная отправка команды...\n"
|
|
||||||
)
|
|
||||||
logging.warning(f"Ошибка в команде: {cmd}. Попытка повторной отправки.")
|
|
||||||
attempt += 1
|
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.append_interactive_text(f"[INFO] Ответ устройства:\n{response}\n")
|
|
||||||
logging.info(f"Получен ответ:\n{response}")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.append_interactive_text("[WARN] Ответ не получен.\n")
|
|
||||||
logging.warning("Нет ответа от устройства в течение таймаута.")
|
|
||||||
break
|
|
||||||
if attempt == max_attempts:
|
|
||||||
self.append_interactive_text(f"[ERROR] Команда не выполнена корректно после {max_attempts} попыток: {cmd}\n")
|
|
||||||
logging.error(f"Команда не выполнена корректно после {max_attempts} попыток: {cmd}")
|
|
||||||
except SerialException as e:
|
|
||||||
self.append_interactive_text(f"[ERROR] Ошибка при отправке команды: {e}\n")
|
|
||||||
logging.error(f"Ошибка отправки команды: {e}", exc_info=True)
|
|
||||||
except Exception as e:
|
|
||||||
self.append_interactive_text(f"[ERROR] Неизвестная ошибка: {e}\n")
|
|
||||||
logging.error(f"Неизвестная ошибка: {e}", exc_info=True)
|
|
||||||
|
|
||||||
def append_interactive_text(self, text):
|
|
||||||
self.interactive_text.insert(END, text)
|
|
||||||
self.interactive_text.see(END)
|
|
||||||
|
|
||||||
# -------------- Вкладка "Выполнить команды из файла" --------------
|
|
||||||
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 "")
|
|
||||||
ttk.Entry(file_frame, textvariable=self.file_exec_var, width=40).pack(side=LEFT, padx=5)
|
|
||||||
ttk.Button(file_frame, text="Выбрать", command=self.select_config_file_fileexec).pack(side=LEFT, padx=5)
|
|
||||||
ttk.Button(frame, text="Выполнить команды", command=self.execute_file_commands).pack(pady=5)
|
|
||||||
self.file_exec_text = tk.Text(frame, wrap="word", height=15)
|
|
||||||
self.file_exec_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
def select_config_file_fileexec(self):
|
|
||||||
filename = filedialog.askopenfilename(title="Выберите файл конфигурации", filetypes=[("Text files", "*.txt")])
|
|
||||||
if filename:
|
|
||||||
self.file_exec_var.set(filename)
|
|
||||||
|
|
||||||
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 not self.connection:
|
|
||||||
self.append_file_exec_text("[ERROR] Не удалось установить соединение.\n")
|
|
||||||
return
|
|
||||||
threading.Thread(
|
|
||||||
target=execute_commands_from_file,
|
|
||||||
args=(
|
|
||||||
self.connection,
|
|
||||||
self.file_exec_var.get(),
|
|
||||||
self.settings.get("timeout", 10),
|
|
||||||
self.settings.get("copy_mode", "line"),
|
|
||||||
self.settings.get("block_size", 15),
|
|
||||||
self.append_file_exec_text,
|
|
||||||
self.settings.get("login"),
|
|
||||||
self.settings.get("password"),
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
daemon=True,
|
|
||||||
).start()
|
|
||||||
|
|
||||||
def append_file_exec_text(self, text):
|
|
||||||
self.file_exec_text.insert(END, text)
|
|
||||||
self.file_exec_text.see(END)
|
|
||||||
|
|
||||||
# -------------- Вкладка "Редактор конфигурационного файла" --------------
|
|
||||||
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 "")
|
|
||||||
ttk.Entry(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 = tk.Text(frame, wrap="word")
|
|
||||||
self.config_editor_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
def select_config_file_editor(self):
|
|
||||||
filename = filedialog.askopenfilename(title="Выберите файл конфигурации", filetypes=[("Text files", "*.txt")])
|
|
||||||
if filename:
|
|
||||||
self.editor_file_var.set(filename)
|
|
||||||
self.settings["config_file"] = filename
|
|
||||||
settings_save(self.settings)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Парсер аргументов (не используется)
|
|
||||||
# ==========================
|
|
||||||
# def parse_arguments():
|
|
||||||
# parser = argparse.ArgumentParser(
|
|
||||||
# description="Программа для работы с устройствами через последовательный порт с графическим интерфейсом."
|
|
||||||
# )
|
|
||||||
# parser.add_argument("--cli", action="store_true", help="Запустить в режиме командной строки (без графики)")
|
|
||||||
# return parser.parse_args()
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Режим командной строки (не используется)
|
|
||||||
# ==========================
|
|
||||||
|
|
||||||
# def run_cli_mode(settings):
|
|
||||||
# print("Запущен режим командной строки.")
|
|
||||||
# while True:
|
|
||||||
# print("\n--- Главное меню ---")
|
|
||||||
# print("1. Подключиться и войти в интерактивный режим")
|
|
||||||
# print("2. Выполнить команды из файла конфигурации")
|
|
||||||
# print("0. Выход")
|
|
||||||
# choice = input("Выберите действие: ").strip()
|
|
||||||
# if choice == "1":
|
|
||||||
# if not settings.get("port"):
|
|
||||||
# print("[ERROR] COM-порт не выбран!")
|
|
||||||
# continue
|
|
||||||
# conn = create_connection(settings)
|
|
||||||
# if not conn:
|
|
||||||
# print("[ERROR] Не удалось установить соединение.")
|
|
||||||
# continue
|
|
||||||
# print("[INFO] Введите команды (для выхода введите _exit):")
|
|
||||||
# while True:
|
|
||||||
# cmd = input("Команда: ")
|
|
||||||
# if cmd.strip().lower() == "_exit":
|
|
||||||
# break
|
|
||||||
# try:
|
|
||||||
# conn.write((cmd + "\n").encode())
|
|
||||||
# response = read_response(
|
|
||||||
# conn, settings.get("timeout", 10),
|
|
||||||
# login=settings.get("login"),
|
|
||||||
# password=settings.get("password"),
|
|
||||||
# is_gui=False
|
|
||||||
# )
|
|
||||||
# print(response)
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f"[ERROR] {e}")
|
|
||||||
# break
|
|
||||||
# conn.close()
|
|
||||||
# elif choice == "2":
|
|
||||||
# if not settings.get("config_file"):
|
|
||||||
# print("[ERROR] Файл конфигурации не выбран!")
|
|
||||||
# continue
|
|
||||||
# if not settings.get("port"):
|
|
||||||
# print("[ERROR] COM-порт не выбран!")
|
|
||||||
# continue
|
|
||||||
# conn = create_connection(settings)
|
|
||||||
# if conn:
|
|
||||||
# execute_commands_from_file(
|
|
||||||
# conn,
|
|
||||||
# settings["config_file"],
|
|
||||||
# settings.get("timeout", 10),
|
|
||||||
# settings.get("copy_mode", "line"),
|
|
||||||
# settings.get("block_size", 15),
|
|
||||||
# lambda msg: print(msg),
|
|
||||||
# login=settings.get("login"),
|
|
||||||
# password=settings.get("password"),
|
|
||||||
# is_gui=False,
|
|
||||||
# )
|
|
||||||
# conn.close()
|
|
||||||
# else:
|
|
||||||
# print("[ERROR] Не удалось установить соединение.")
|
|
||||||
# elif choice == "0":
|
|
||||||
# break
|
|
||||||
# else:
|
|
||||||
# print("[ERROR] Некорректный выбор.")
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Основной запуск приложения
|
|
||||||
# ==========================
|
|
||||||
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)
|
|
||||||
46
README.md
Normal file
46
README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# ComConfigCopy
|
||||||
|
|
||||||
|
Исходный код для версии 1.1. Работоспособность не гарантирована, работа ведётся.
|
||||||
|
|
||||||
|
Программа для копирования конфигураций на коммутаторы.
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
ComConfigCopy - это утилита, разработанная для автоматизации процесса копирования конфигураций на сетевые коммутаторы. Программа предоставляет удобный графический интерфейс для управления процессом копирования и настройки параметров подключения.
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
- Копирование конфигураций на коммутаторы через COM-порт
|
||||||
|
- Поддержка различных скоростей подключения
|
||||||
|
- Автоматическое определение доступных COM-портов
|
||||||
|
- Возможность сохранения и загрузки настроек
|
||||||
|
- Автоматическое обновление через GitHub
|
||||||
|
|
||||||
|
## Системные требования
|
||||||
|
|
||||||
|
- Windows 7/8/10/11
|
||||||
|
- Python 3.8 или выше
|
||||||
|
- Доступ к COM-портам
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Скачайте последнюю версию программы из [репозитория](https://gitea.filow.ru/LowaSC/ComConfigCopy/releases)
|
||||||
|
2. Распакуйте архив в удобное место
|
||||||
|
3. Запустите файл `ComConfigCopy.exe`
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
1. Выберите COM-порт из списка доступных
|
||||||
|
2. Настройте параметры подключения (скорость, биты данных и т.д.)
|
||||||
|
3. Выберите файл конфигурации для отправки
|
||||||
|
4. Нажмите кнопку "Отправить" для начала процесса копирования
|
||||||
|
|
||||||
|
## Контакты
|
||||||
|
|
||||||
|
- Email: LowaWorkMail@gmail.com
|
||||||
|
- Telegram: [@LowaSC](https://t.me/LowaSC)
|
||||||
|
- Репозиторий: [ComConfigCopy](https://gitea.filow.ru/LowaSC/ComConfigCopy)
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Этот проект распространяется под лицензией MIT. Подробности смотрите в файле [LICENSE](LICENSE).
|
||||||
100
about_window.py
100
about_window.py
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, BOTH, X, BOTTOM
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
class AboutWindow(tk.Toplevel):
|
|
||||||
def __init__(self, parent):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.title("О программе")
|
|
||||||
self.geometry("400x300")
|
|
||||||
self.resizable(False, False)
|
|
||||||
|
|
||||||
# Создаем фрейм
|
|
||||||
about_frame = ttk.Frame(self, padding="20")
|
|
||||||
about_frame.pack(fill=BOTH, expand=True)
|
|
||||||
|
|
||||||
# Заголовок
|
|
||||||
ttk.Label(
|
|
||||||
about_frame,
|
|
||||||
text="ComConfigCopy",
|
|
||||||
font=("Segoe UI", 16, "bold")
|
|
||||||
).pack(pady=(0, 10))
|
|
||||||
|
|
||||||
# Описание
|
|
||||||
ttk.Label(
|
|
||||||
about_frame,
|
|
||||||
text="Программа для копирования конфигураций на коммутаторы",
|
|
||||||
wraplength=350
|
|
||||||
).pack(pady=(0, 20))
|
|
||||||
|
|
||||||
# Версия
|
|
||||||
ttk.Label(
|
|
||||||
about_frame,
|
|
||||||
text="Версия 1.0",
|
|
||||||
font=("Segoe UI", 10)
|
|
||||||
).pack(pady=(0, 20))
|
|
||||||
|
|
||||||
# Контактная информация
|
|
||||||
contact_frame = ttk.Frame(about_frame)
|
|
||||||
contact_frame.pack(fill=X, pady=(0, 20))
|
|
||||||
|
|
||||||
# Исходный код
|
|
||||||
ttk.Label(
|
|
||||||
contact_frame,
|
|
||||||
text="Исходный код:",
|
|
||||||
font=("Segoe UI", 10, "bold")
|
|
||||||
).pack(anchor="w")
|
|
||||||
|
|
||||||
source_link = ttk.Label(
|
|
||||||
contact_frame,
|
|
||||||
text="https://gitea.filow.ru/LowaSC/ComConfigCopy",
|
|
||||||
cursor="hand2",
|
|
||||||
foreground="blue"
|
|
||||||
)
|
|
||||||
source_link.pack(anchor="w")
|
|
||||||
source_link.bind("<Button-1>", lambda e: self.open_url("https://gitea.filow.ru/LowaSC/ComConfigCopy"))
|
|
||||||
|
|
||||||
# Контакты
|
|
||||||
ttk.Label(
|
|
||||||
contact_frame,
|
|
||||||
text="\nКонтакты:",
|
|
||||||
font=("Segoe UI", 10, "bold")
|
|
||||||
).pack(anchor="w")
|
|
||||||
|
|
||||||
ttk.Label(
|
|
||||||
contact_frame,
|
|
||||||
text="Email: LowaWorkMail@gmail.com"
|
|
||||||
).pack(anchor="w")
|
|
||||||
|
|
||||||
telegram_link = ttk.Label(
|
|
||||||
contact_frame,
|
|
||||||
text="Telegram: @LowaSC",
|
|
||||||
cursor="hand2",
|
|
||||||
foreground="blue"
|
|
||||||
)
|
|
||||||
telegram_link.pack(anchor="w")
|
|
||||||
telegram_link.bind("<Button-1>", lambda e: self.open_url("https://t.me/LowaSC"))
|
|
||||||
|
|
||||||
# Кнопка закрытия
|
|
||||||
ttk.Button(
|
|
||||||
about_frame,
|
|
||||||
text="Закрыть",
|
|
||||||
command=self.destroy
|
|
||||||
).pack(side=BOTTOM, pady=(20, 0))
|
|
||||||
|
|
||||||
# Центрируем окно
|
|
||||||
self.center_window()
|
|
||||||
|
|
||||||
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 open_url(self, url):
|
|
||||||
webbrowser.open(url)
|
|
||||||
Binary file not shown.
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
tftpy>=0.8.0
|
||||||
|
pyserial>=3.5
|
||||||
|
requests>=2.31.0
|
||||||
|
packaging>=23.2
|
||||||
39
src/README.md
Normal file
39
src/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# ComConfigCopy
|
||||||
|
|
||||||
|
Приложение для копирования конфигураций на сетевое оборудование через последовательный порт и TFTP.
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── core/ # Ядро приложения
|
||||||
|
├── communication/ # Коммуникация с устройствами
|
||||||
|
├── filesystem/ # Работа с файловой системой
|
||||||
|
├── network/ # Сетевые компоненты
|
||||||
|
├── ui/ # Пользовательский интерфейс
|
||||||
|
└── utils/ # Утилиты
|
||||||
|
```
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- pyserial>=3.5
|
||||||
|
- tftpy>=0.8.0
|
||||||
|
- requests>=2.31.0
|
||||||
|
- watchdog>=3.0.0
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий
|
||||||
|
2. Создайте виртуальное окружение: `python -m venv .venv`
|
||||||
|
3. Активируйте виртуальное окружение:
|
||||||
|
- Windows: `.venv\Scripts\activate`
|
||||||
|
- Linux/Mac: `source .venv/bin/activate`
|
||||||
|
4. Установите зависимости: `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Запустите приложение:
|
||||||
|
```bash
|
||||||
|
python -m src.core.app
|
||||||
|
```
|
||||||
0
src/communication/__init__.py
Normal file
0
src/communication/__init__.py
Normal file
174
src/communication/command_handler.py
Normal file
174
src/communication/command_handler.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.exceptions import ValidationError
|
||||||
|
from .serial_manager import SerialManager
|
||||||
|
|
||||||
|
class CommandHandler:
|
||||||
|
"""Обработчик команд для работы с устройством."""
|
||||||
|
|
||||||
|
def __init__(self, serial_manager: SerialManager):
|
||||||
|
self._serial = serial_manager
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._command_cache: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def validate_command(self, command: str) -> None:
|
||||||
|
"""
|
||||||
|
Валидация команды перед отправкой.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Команда для проверки
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: Если команда не прошла валидацию
|
||||||
|
"""
|
||||||
|
# Проверяем на пустую команду
|
||||||
|
if not command or not command.strip():
|
||||||
|
raise ValidationError("Пустая команда")
|
||||||
|
|
||||||
|
# Проверяем на недопустимые символы
|
||||||
|
if re.search(r'[\x00-\x1F\x7F]', command):
|
||||||
|
raise ValidationError("Команда содержит недопустимые символы")
|
||||||
|
|
||||||
|
# Проверяем максимальную длину
|
||||||
|
if len(command) > 1024:
|
||||||
|
raise ValidationError("Превышена максимальная длина команды (1024 символа)")
|
||||||
|
|
||||||
|
def validate_config_commands(self, commands: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Валидация списка команд конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commands: Список команд для проверки
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: Если команды не прошли валидацию
|
||||||
|
"""
|
||||||
|
if not commands:
|
||||||
|
raise ValidationError("Пустой список команд")
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
self.validate_command(command)
|
||||||
|
|
||||||
|
def execute_command(self, command: str, timeout: Optional[int] = None,
|
||||||
|
use_cache: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Выполнение команды на устройстве.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Команда для выполнения
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
use_cache: Использовать кэш команд
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: При ошибке валидации
|
||||||
|
ConnectionError: При ошибке соединения
|
||||||
|
"""
|
||||||
|
# Проверяем команду
|
||||||
|
self.validate_command(command)
|
||||||
|
|
||||||
|
# Проверяем кэш
|
||||||
|
if use_cache and command in self._command_cache:
|
||||||
|
self._logger.debug(f"Использован кэш для команды: {command}")
|
||||||
|
return self._command_cache[command]
|
||||||
|
|
||||||
|
# Выполняем команду
|
||||||
|
response = self._serial.send_command(command, timeout)
|
||||||
|
|
||||||
|
# Сохраняем в кэш
|
||||||
|
if use_cache:
|
||||||
|
self._command_cache[command] = response
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def execute_config(self, commands: List[str], timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Выполнение конфигурационных команд на устройстве.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commands: Список команд для выполнения
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: При ошибке валидации
|
||||||
|
ConnectionError: При ошибке соединения
|
||||||
|
"""
|
||||||
|
# Проверяем команды
|
||||||
|
self.validate_config_commands(commands)
|
||||||
|
|
||||||
|
# Выполняем команды
|
||||||
|
return self._serial.send_config(commands, timeout)
|
||||||
|
|
||||||
|
def clear_cache(self) -> None:
|
||||||
|
"""Очистка кэша команд."""
|
||||||
|
self._command_cache.clear()
|
||||||
|
self._logger.debug("Кэш команд очищен")
|
||||||
|
|
||||||
|
def get_device_info(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Получение информации об устройстве.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, str]: Словарь с информацией об устройстве
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке соединения
|
||||||
|
"""
|
||||||
|
info = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем версию ПО
|
||||||
|
version = self.execute_command("show version", use_cache=True)
|
||||||
|
info["version"] = self._parse_version(version)
|
||||||
|
|
||||||
|
# Получаем hostname
|
||||||
|
hostname = self.execute_command("show hostname", use_cache=True)
|
||||||
|
info["hostname"] = hostname.strip()
|
||||||
|
|
||||||
|
# Получаем модель
|
||||||
|
model = self.execute_command("show model", use_cache=True)
|
||||||
|
info["model"] = self._parse_model(model)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка получения информации об устройстве: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _parse_version(self, version_output: str) -> str:
|
||||||
|
"""
|
||||||
|
Парсинг версии из вывода команды.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_output: Вывод команды show version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Версия ПО
|
||||||
|
"""
|
||||||
|
# Здесь должна быть реализация парсинга версии
|
||||||
|
# в зависимости от формата вывода конкретного устройства
|
||||||
|
return version_output.strip()
|
||||||
|
|
||||||
|
def _parse_model(self, model_output: str) -> str:
|
||||||
|
"""
|
||||||
|
Парсинг модели из вывода команды.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_output: Вывод команды show model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Модель устройства
|
||||||
|
"""
|
||||||
|
# Здесь должна быть реализация парсинга модели
|
||||||
|
# в зависимости от формата вывода конкретного устройства
|
||||||
|
return model_output.strip()
|
||||||
119
src/communication/protocols/base.py
Normal file
119
src/communication/protocols/base.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
from core.exceptions import ConnectionError, AuthenticationError
|
||||||
|
|
||||||
|
class BaseProtocol(ABC):
|
||||||
|
"""Базовый класс для всех протоколов связи."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
self._config: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Проверка состояния подключения."""
|
||||||
|
return self._connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self) -> bool:
|
||||||
|
"""Проверка состояния аутентификации."""
|
||||||
|
return self._authenticated
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""
|
||||||
|
Установка соединения.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке подключения
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Разрыв соединения."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def authenticate(self, username: str, password: str) -> None:
|
||||||
|
"""
|
||||||
|
Аутентификация на устройстве.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Имя пользователя
|
||||||
|
password: Пароль
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthenticationError: При ошибке аутентификации
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_command(self, command: str, timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Отправка команды на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Команда для отправки
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке отправки команды
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_config(self, config_commands: list[str], timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Отправка конфигурационных команд на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_commands: Список команд конфигурации
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке отправки команд
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def configure(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Конфигурация протокола.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Параметры конфигурации
|
||||||
|
"""
|
||||||
|
self._config.update(kwargs)
|
||||||
|
|
||||||
|
def _notify_connection_established(self) -> None:
|
||||||
|
"""Уведомление об установке соединения."""
|
||||||
|
self._connected = True
|
||||||
|
event_bus.publish(Event(EventTypes.CONNECTION_ESTABLISHED, None))
|
||||||
|
|
||||||
|
def _notify_connection_lost(self) -> None:
|
||||||
|
"""Уведомление о потере соединения."""
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
event_bus.publish(Event(EventTypes.CONNECTION_LOST, None))
|
||||||
|
|
||||||
|
def _notify_connection_error(self, error: Exception) -> None:
|
||||||
|
"""
|
||||||
|
Уведомление об ошибке соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error: Объект ошибки
|
||||||
|
"""
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
event_bus.publish(Event(EventTypes.CONNECTION_ERROR, str(error)))
|
||||||
224
src/communication/protocols/serial.py
Normal file
224
src/communication/protocols/serial.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from typing import Optional, List
|
||||||
|
import serial
|
||||||
|
from serial.serialutil import SerialException
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.exceptions import ConnectionError, AuthenticationError
|
||||||
|
from .base import BaseProtocol
|
||||||
|
|
||||||
|
class SerialProtocol(BaseProtocol):
|
||||||
|
"""Протокол для работы с последовательным портом."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._serial: Optional[serial.Serial] = None
|
||||||
|
self._prompt = AppConfig.DEFAULT_PROMPT
|
||||||
|
self._timeout = AppConfig.DEFAULT_TIMEOUT
|
||||||
|
self._baudrate = AppConfig.DEFAULT_BAUDRATE
|
||||||
|
self._port: Optional[str] = None
|
||||||
|
|
||||||
|
def configure(self, port: str, baudrate: int = None, timeout: int = None,
|
||||||
|
prompt: str = None) -> None:
|
||||||
|
"""
|
||||||
|
Конфигурация последовательного порта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: COM-порт
|
||||||
|
baudrate: Скорость передачи
|
||||||
|
timeout: Таймаут операций
|
||||||
|
prompt: Приглашение командной строки
|
||||||
|
"""
|
||||||
|
self._port = port
|
||||||
|
if baudrate is not None:
|
||||||
|
self._baudrate = baudrate
|
||||||
|
if timeout is not None:
|
||||||
|
self._timeout = timeout
|
||||||
|
if prompt is not None:
|
||||||
|
self._prompt = prompt
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""
|
||||||
|
Установка соединения с устройством.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке подключения
|
||||||
|
"""
|
||||||
|
if not self._port:
|
||||||
|
raise ConnectionError("Не указан COM-порт")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._serial = serial.Serial(
|
||||||
|
port=self._port,
|
||||||
|
baudrate=self._baudrate,
|
||||||
|
timeout=1
|
||||||
|
)
|
||||||
|
time.sleep(1) # Ждем инициализации порта
|
||||||
|
self._notify_connection_established()
|
||||||
|
|
||||||
|
except SerialException as e:
|
||||||
|
self._notify_connection_error(e)
|
||||||
|
raise ConnectionError(f"Ошибка подключения к порту {self._port}: {e}")
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Закрытие соединения."""
|
||||||
|
if self._serial and self._serial.is_open:
|
||||||
|
self._serial.close()
|
||||||
|
self._notify_connection_lost()
|
||||||
|
|
||||||
|
def authenticate(self, username: str, password: str) -> None:
|
||||||
|
"""
|
||||||
|
Аутентификация на устройстве.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Имя пользователя
|
||||||
|
password: Пароль
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthenticationError: При ошибке аутентификации
|
||||||
|
ConnectionError: При ошибке соединения
|
||||||
|
"""
|
||||||
|
if not self.is_connected:
|
||||||
|
raise ConnectionError("Нет подключения к устройству")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Очищаем буфер
|
||||||
|
self._serial.reset_input_buffer()
|
||||||
|
self._serial.reset_output_buffer()
|
||||||
|
|
||||||
|
# Отправляем Enter для получения приглашения
|
||||||
|
self._serial.write(b"\n")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Ожидаем запрос логина или пароля
|
||||||
|
response = self._read_until(["login:", "username:", "password:", self._prompt])
|
||||||
|
|
||||||
|
if "login:" in response.lower() or "username:" in response.lower():
|
||||||
|
self._serial.write(f"{username}\n".encode())
|
||||||
|
time.sleep(0.5)
|
||||||
|
response = self._read_until(["password:", self._prompt])
|
||||||
|
|
||||||
|
if "password:" in response.lower():
|
||||||
|
self._serial.write(f"{password}\n".encode())
|
||||||
|
time.sleep(0.5)
|
||||||
|
response = self._read_until([self._prompt])
|
||||||
|
|
||||||
|
if self._prompt not in response:
|
||||||
|
raise AuthenticationError("Неверные учетные данные")
|
||||||
|
|
||||||
|
self._authenticated = True
|
||||||
|
|
||||||
|
except SerialException as e:
|
||||||
|
self._notify_connection_error(e)
|
||||||
|
raise ConnectionError(f"Ошибка при аутентификации: {e}")
|
||||||
|
|
||||||
|
def send_command(self, command: str, timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Отправка команды на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Команда для отправки
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке отправки команды
|
||||||
|
"""
|
||||||
|
if not self.is_connected:
|
||||||
|
raise ConnectionError("Нет подключения к устройству")
|
||||||
|
|
||||||
|
if not self.is_authenticated:
|
||||||
|
raise ConnectionError("Требуется аутентификация")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Очищаем буфер перед отправкой
|
||||||
|
self._serial.reset_input_buffer()
|
||||||
|
|
||||||
|
# Отправляем команду
|
||||||
|
self._serial.write(f"{command}\n".encode())
|
||||||
|
|
||||||
|
# Ожидаем ответ
|
||||||
|
response = self._read_until([self._prompt], timeout or self._timeout)
|
||||||
|
|
||||||
|
# Удаляем отправленную команду из ответа
|
||||||
|
lines = response.splitlines()
|
||||||
|
if lines and command in lines[0]:
|
||||||
|
lines.pop(0)
|
||||||
|
|
||||||
|
return "\n".join(lines).strip()
|
||||||
|
|
||||||
|
except SerialException as e:
|
||||||
|
self._notify_connection_error(e)
|
||||||
|
raise ConnectionError(f"Ошибка при отправке команды: {e}")
|
||||||
|
|
||||||
|
def send_config(self, config_commands: List[str], timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Отправка конфигурационных команд на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_commands: Список команд конфигурации
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке отправки команд
|
||||||
|
"""
|
||||||
|
responses = []
|
||||||
|
for command in config_commands:
|
||||||
|
response = self.send_command(command, timeout)
|
||||||
|
responses.append(response)
|
||||||
|
return "\n".join(responses)
|
||||||
|
|
||||||
|
def _read_until(self, patterns: List[str], timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Чтение данных из порта до появления одного из паттернов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
patterns: Список паттернов для поиска
|
||||||
|
timeout: Таймаут операции
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Прочитанные данные
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке чтения или таймауте
|
||||||
|
"""
|
||||||
|
if not patterns:
|
||||||
|
raise ValueError("Не указаны паттерны для поиска")
|
||||||
|
|
||||||
|
timeout = timeout or self._timeout
|
||||||
|
end_time = time.time() + timeout
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
|
while time.time() < end_time:
|
||||||
|
if self._serial.in_waiting:
|
||||||
|
chunk = self._serial.read(self._serial.in_waiting).decode(errors="ignore")
|
||||||
|
buffer += chunk
|
||||||
|
|
||||||
|
# Проверяем наличие паттернов
|
||||||
|
for pattern in patterns:
|
||||||
|
if pattern in buffer:
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
# Небольшая задержка для снижения нагрузки на CPU
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
raise ConnectionError(f"Таймаут операции ({timeout} сек)")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_ports() -> List[str]:
|
||||||
|
"""
|
||||||
|
Получение списка доступных COM-портов.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Список доступных портов
|
||||||
|
"""
|
||||||
|
return [port.device for port in serial.tools.list_ports.comports()]
|
||||||
190
src/communication/protocols/tftp.py
Normal file
190
src/communication/protocols/tftp.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional, Callable
|
||||||
|
import tftpy
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.exceptions import TFTPError
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
class TFTPProtocol:
|
||||||
|
"""Протокол для работы с TFTP сервером."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._server: Optional[tftpy.TftpServer] = None
|
||||||
|
self._client: Optional[tftpy.TftpClient] = None
|
||||||
|
self._server_running = False
|
||||||
|
self._root_dir = AppConfig.CONFIGS_DIR
|
||||||
|
self._port = AppConfig.TFTP_PORT
|
||||||
|
self._timeout = AppConfig.TFTP_TIMEOUT
|
||||||
|
self._retries = AppConfig.TFTP_RETRIES
|
||||||
|
|
||||||
|
def configure(self, root_dir: Optional[str] = None, port: Optional[int] = None,
|
||||||
|
timeout: Optional[int] = None, retries: Optional[int] = None) -> None:
|
||||||
|
"""
|
||||||
|
Конфигурация TFTP сервера/клиента.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root_dir: Корневая директория для файлов
|
||||||
|
port: Порт TFTP сервера
|
||||||
|
timeout: Таймаут операций
|
||||||
|
retries: Количество попыток
|
||||||
|
"""
|
||||||
|
if root_dir is not None:
|
||||||
|
self._root_dir = root_dir
|
||||||
|
if port is not None:
|
||||||
|
self._port = port
|
||||||
|
if timeout is not None:
|
||||||
|
self._timeout = timeout
|
||||||
|
if retries is not None:
|
||||||
|
self._retries = retries
|
||||||
|
|
||||||
|
def start_server(self, host: str = "0.0.0.0") -> None:
|
||||||
|
"""
|
||||||
|
Запуск TFTP сервера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: IP-адрес для прослушивания
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TFTPError: При ошибке запуска сервера
|
||||||
|
"""
|
||||||
|
if self._server_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем серверный объект
|
||||||
|
self._server = tftpy.TftpServer(self._root_dir)
|
||||||
|
|
||||||
|
# Запускаем сервер в отдельном потоке
|
||||||
|
self._server.listen(host, self._port, timeout=self._timeout)
|
||||||
|
self._server_running = True
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_SERVER_STARTED, {
|
||||||
|
"host": host,
|
||||||
|
"port": self._port,
|
||||||
|
"root_dir": self._root_dir
|
||||||
|
}))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise TFTPError(f"Ошибка запуска TFTP сервера: {e}")
|
||||||
|
|
||||||
|
def stop_server(self) -> None:
|
||||||
|
"""Остановка TFTP сервера."""
|
||||||
|
if self._server and self._server_running:
|
||||||
|
self._server.stop()
|
||||||
|
self._server = None
|
||||||
|
self._server_running = False
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_SERVER_STOPPED, None))
|
||||||
|
|
||||||
|
def upload_file(self, filename: str, host: str, remote_filename: Optional[str] = None,
|
||||||
|
progress_callback: Optional[Callable[[int], None]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Загрузка файла на удаленное устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Путь к локальному файлу
|
||||||
|
host: IP-адрес устройства
|
||||||
|
remote_filename: Имя файла на устройстве
|
||||||
|
progress_callback: Функция обратного вызова для отслеживания прогресса
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TFTPError: При ошибке загрузки файла
|
||||||
|
"""
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
raise TFTPError(f"Файл не найден: {filename}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем клиентский объект
|
||||||
|
self._client = tftpy.TftpClient(
|
||||||
|
host,
|
||||||
|
self._port,
|
||||||
|
options={"timeout": self._timeout, "retries": self._retries}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Определяем имя удаленного файла
|
||||||
|
if not remote_filename:
|
||||||
|
remote_filename = os.path.basename(filename)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_STARTED, {
|
||||||
|
"operation": "upload",
|
||||||
|
"local_file": filename,
|
||||||
|
"remote_file": remote_filename,
|
||||||
|
"host": host
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Загружаем файл
|
||||||
|
self._client.upload(
|
||||||
|
remote_filename,
|
||||||
|
filename,
|
||||||
|
progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_COMPLETED, {
|
||||||
|
"operation": "upload",
|
||||||
|
"local_file": filename,
|
||||||
|
"remote_file": remote_filename,
|
||||||
|
"host": host
|
||||||
|
}))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_ERROR, str(e)))
|
||||||
|
raise TFTPError(f"Ошибка загрузки файла: {e}")
|
||||||
|
|
||||||
|
def download_file(self, remote_filename: str, host: str, local_filename: Optional[str] = None,
|
||||||
|
progress_callback: Optional[Callable[[int], None]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Загрузка файла с удаленного устройства.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote_filename: Имя файла на устройстве
|
||||||
|
host: IP-адрес устройства
|
||||||
|
local_filename: Путь для сохранения файла
|
||||||
|
progress_callback: Функция обратного вызова для отслеживания прогресса
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TFTPError: При ошибке загрузки файла
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Создаем клиентский объект
|
||||||
|
self._client = tftpy.TftpClient(
|
||||||
|
host,
|
||||||
|
self._port,
|
||||||
|
options={"timeout": self._timeout, "retries": self._retries}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Определяем имя локального файла
|
||||||
|
if not local_filename:
|
||||||
|
local_filename = os.path.join(self._root_dir, remote_filename)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_STARTED, {
|
||||||
|
"operation": "download",
|
||||||
|
"local_file": local_filename,
|
||||||
|
"remote_file": remote_filename,
|
||||||
|
"host": host
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Загружаем файл
|
||||||
|
self._client.download(
|
||||||
|
remote_filename,
|
||||||
|
local_filename,
|
||||||
|
progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_TRANSFER_COMPLETED, {
|
||||||
|
"operation": "download",
|
||||||
|
"local_file": local_filename,
|
||||||
|
"remote_file": remote_filename,
|
||||||
|
"host": host
|
||||||
|
}))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_ERROR, str(e)))
|
||||||
|
raise TFTPError(f"Ошибка загрузки файла: {e}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_server_running(self) -> bool:
|
||||||
|
"""Проверка состояния сервера."""
|
||||||
|
return self._server_running
|
||||||
223
src/communication/response_parser.py
Normal file
223
src/communication/response_parser.py
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParsedResponse:
|
||||||
|
"""Структура разобранного ответа."""
|
||||||
|
success: bool
|
||||||
|
data: Any
|
||||||
|
error: Optional[str] = None
|
||||||
|
raw_response: Optional[str] = None
|
||||||
|
|
||||||
|
class ResponseParser:
|
||||||
|
"""Парсер ответов от сетевого устройства."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Регулярные выражения для парсинга
|
||||||
|
self._patterns = {
|
||||||
|
"error": r"(?i)error|invalid|failed|denied|rejected",
|
||||||
|
"success": r"(?i)success|completed|done|ok",
|
||||||
|
"version": r"(?i)version\s+(\S+)",
|
||||||
|
"model": r"(?i)model\s*:\s*(\S+)",
|
||||||
|
"hostname": r"(?i)hostname\s*:\s*(\S+)",
|
||||||
|
"interface": r"(?i)interface\s+(\S+)",
|
||||||
|
"ip_address": r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})",
|
||||||
|
"mac_address": r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})",
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_command_response(self, response: str) -> ParsedResponse:
|
||||||
|
"""
|
||||||
|
Парсинг ответа на команду.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Ответ устройства
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedResponse: Структура с результатами парсинга
|
||||||
|
"""
|
||||||
|
# Проверяем на наличие ошибок
|
||||||
|
if re.search(self._patterns["error"], response, re.MULTILINE):
|
||||||
|
error_msg = self._extract_error_message(response)
|
||||||
|
return ParsedResponse(
|
||||||
|
success=False,
|
||||||
|
data=None,
|
||||||
|
error=error_msg,
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем на успешное выполнение
|
||||||
|
success = bool(re.search(self._patterns["success"], response, re.MULTILINE))
|
||||||
|
|
||||||
|
return ParsedResponse(
|
||||||
|
success=success,
|
||||||
|
data=response.strip(),
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_version(self, response: str) -> ParsedResponse:
|
||||||
|
"""
|
||||||
|
Парсинг версии ПО.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Ответ на команду show version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedResponse: Структура с результатами парсинга
|
||||||
|
"""
|
||||||
|
match = re.search(self._patterns["version"], response)
|
||||||
|
if match:
|
||||||
|
return ParsedResponse(
|
||||||
|
success=True,
|
||||||
|
data=match.group(1),
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
return ParsedResponse(
|
||||||
|
success=False,
|
||||||
|
data=None,
|
||||||
|
error="Версия не найдена",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_model(self, response: str) -> ParsedResponse:
|
||||||
|
"""
|
||||||
|
Парсинг модели устройства.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Ответ на команду show model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedResponse: Структура с результатами парсинга
|
||||||
|
"""
|
||||||
|
match = re.search(self._patterns["model"], response)
|
||||||
|
if match:
|
||||||
|
return ParsedResponse(
|
||||||
|
success=True,
|
||||||
|
data=match.group(1),
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
return ParsedResponse(
|
||||||
|
success=False,
|
||||||
|
data=None,
|
||||||
|
error="Модель не найдена",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_interfaces(self, response: str) -> ParsedResponse:
|
||||||
|
"""
|
||||||
|
Парсинг информации об интерфейсах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Ответ на команду show interfaces
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedResponse: Структура с результатами парсинга
|
||||||
|
"""
|
||||||
|
interfaces = []
|
||||||
|
|
||||||
|
# Ищем все интерфейсы
|
||||||
|
for line in response.splitlines():
|
||||||
|
if_match = re.search(self._patterns["interface"], line)
|
||||||
|
if if_match:
|
||||||
|
interface = {
|
||||||
|
"name": if_match.group(1),
|
||||||
|
"ip": None,
|
||||||
|
"mac": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ищем IP-адрес
|
||||||
|
ip_match = re.search(self._patterns["ip_address"], line)
|
||||||
|
if ip_match:
|
||||||
|
interface["ip"] = ip_match.group(1)
|
||||||
|
|
||||||
|
# Ищем MAC-адрес
|
||||||
|
mac_match = re.search(self._patterns["mac_address"], line)
|
||||||
|
if mac_match:
|
||||||
|
interface["mac"] = mac_match.group(0)
|
||||||
|
|
||||||
|
interfaces.append(interface)
|
||||||
|
|
||||||
|
if interfaces:
|
||||||
|
return ParsedResponse(
|
||||||
|
success=True,
|
||||||
|
data=interfaces,
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
return ParsedResponse(
|
||||||
|
success=False,
|
||||||
|
data=None,
|
||||||
|
error="Интерфейсы не найдены",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_config_result(self, response: str) -> ParsedResponse:
|
||||||
|
"""
|
||||||
|
Парсинг результата применения конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Ответ на команды конфигурации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParsedResponse: Структура с результатами парсинга
|
||||||
|
"""
|
||||||
|
# Проверяем на наличие ошибок
|
||||||
|
if re.search(self._patterns["error"], response, re.MULTILINE):
|
||||||
|
error_msg = self._extract_error_message(response)
|
||||||
|
return ParsedResponse(
|
||||||
|
success=False,
|
||||||
|
data=None,
|
||||||
|
error=error_msg,
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
# Если нет ошибок, считаем что конфигурация применена успешно
|
||||||
|
return ParsedResponse(
|
||||||
|
success=True,
|
||||||
|
data=response.strip(),
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_error_message(self, response: str) -> str:
|
||||||
|
"""
|
||||||
|
Извлечение сообщения об ошибке из ответа.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Ответ устройства
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Сообщение об ошибке
|
||||||
|
"""
|
||||||
|
# Ищем строку с ошибкой
|
||||||
|
for line in response.splitlines():
|
||||||
|
if re.search(self._patterns["error"], line, re.IGNORECASE):
|
||||||
|
return line.strip()
|
||||||
|
return "Неизвестная ошибка"
|
||||||
|
|
||||||
|
def add_pattern(self, name: str, pattern: str) -> None:
|
||||||
|
"""
|
||||||
|
Добавление нового шаблона для парсинга.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя шаблона
|
||||||
|
pattern: Регулярное выражение
|
||||||
|
"""
|
||||||
|
self._patterns[name] = pattern
|
||||||
|
self._logger.debug(f"Добавлен новый шаблон: {name} = {pattern}")
|
||||||
|
|
||||||
|
def get_pattern(self, name: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получение шаблона по имени.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя шаблона
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Регулярное выражение или None
|
||||||
|
"""
|
||||||
|
return self._patterns.get(name)
|
||||||
148
src/communication/serial_manager.py
Normal file
148
src/communication/serial_manager.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
from core.exceptions import ConnectionError, AuthenticationError
|
||||||
|
from core.config import AppConfig
|
||||||
|
from .protocols.serial import SerialProtocol
|
||||||
|
|
||||||
|
class SerialManager:
|
||||||
|
"""Менеджер для работы с последовательным портом."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._protocol = SerialProtocol()
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
|
||||||
|
def configure(self, settings: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Конфигурация последовательного порта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
settings: Словарь с настройками
|
||||||
|
"""
|
||||||
|
self._protocol.configure(
|
||||||
|
port=settings.get("port"),
|
||||||
|
baudrate=settings.get("baudrate", AppConfig.DEFAULT_BAUDRATE),
|
||||||
|
timeout=settings.get("timeout", AppConfig.DEFAULT_TIMEOUT),
|
||||||
|
prompt=settings.get("prompt", AppConfig.DEFAULT_PROMPT)
|
||||||
|
)
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""
|
||||||
|
Установка соединения с устройством.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке подключения
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._protocol.connect()
|
||||||
|
self._connected = True
|
||||||
|
self._logger.info("Соединение установлено")
|
||||||
|
|
||||||
|
except ConnectionError as e:
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
self._logger.error(f"Ошибка подключения: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Разрыв соединения."""
|
||||||
|
if self._connected:
|
||||||
|
self._protocol.disconnect()
|
||||||
|
self._connected = False
|
||||||
|
self._authenticated = False
|
||||||
|
self._logger.info("Соединение разорвано")
|
||||||
|
|
||||||
|
def authenticate(self, username: str, password: str) -> None:
|
||||||
|
"""
|
||||||
|
Аутентификация на устройстве.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Имя пользователя
|
||||||
|
password: Пароль
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке соединения
|
||||||
|
AuthenticationError: При ошибке аутентификации
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._protocol.authenticate(username, password)
|
||||||
|
self._authenticated = True
|
||||||
|
self._logger.info("Аутентификация успешна")
|
||||||
|
|
||||||
|
except (ConnectionError, AuthenticationError) as e:
|
||||||
|
self._authenticated = False
|
||||||
|
self._logger.error(f"Ошибка аутентификации: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send_command(self, command: str, timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Отправка команды на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Команда для отправки
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке отправки команды
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self._protocol.send_command(command, timeout)
|
||||||
|
self._logger.debug(f"Отправлена команда: {command}")
|
||||||
|
self._logger.debug(f"Получен ответ: {response}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
except ConnectionError as e:
|
||||||
|
self._logger.error(f"Ошибка отправки команды: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send_config(self, config_commands: List[str], timeout: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Отправка конфигурационных команд на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_commands: Список команд конфигурации
|
||||||
|
timeout: Таймаут ожидания ответа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Ответ устройства
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConnectionError: При ошибке отправки команд
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self._protocol.send_config(config_commands, timeout)
|
||||||
|
self._logger.info(f"Отправлено {len(config_commands)} команд конфигурации")
|
||||||
|
return response
|
||||||
|
|
||||||
|
except ConnectionError as e:
|
||||||
|
self._logger.error(f"Ошибка отправки конфигурации: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Проверка состояния подключения."""
|
||||||
|
return self._connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self) -> bool:
|
||||||
|
"""Проверка состояния аутентификации."""
|
||||||
|
return self._authenticated
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_ports() -> List[str]:
|
||||||
|
"""
|
||||||
|
Получение списка доступных COM-портов.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Список доступных портов
|
||||||
|
"""
|
||||||
|
return SerialProtocol.list_ports()
|
||||||
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
298
src/core/app.py
Normal file
298
src/core/app.py
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
# Добавляем корневую директорию в PYTHONPATH
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from ui.main_window import MainWindow
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.state import StateManager
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
from filesystem.logger import setup_logging
|
||||||
|
from filesystem.settings import Settings
|
||||||
|
from filesystem.config_manager import ConfigManager
|
||||||
|
from filesystem.watchers.config_watcher import ConfigWatcher
|
||||||
|
from communication.serial_manager import SerialManager
|
||||||
|
from communication.command_handler import CommandHandler
|
||||||
|
from network.servers.tftp_server import TFTPServer
|
||||||
|
from network.transfer.transfer_manager import TransferManager
|
||||||
|
|
||||||
|
class App:
|
||||||
|
"""Основной класс приложения."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._window: Optional[MainWindow] = None
|
||||||
|
|
||||||
|
# Инициализируем компоненты
|
||||||
|
self._init_components()
|
||||||
|
|
||||||
|
# Подписываемся на события
|
||||||
|
self._setup_event_handlers()
|
||||||
|
|
||||||
|
def _init_components(self) -> None:
|
||||||
|
"""Инициализация компонентов приложения."""
|
||||||
|
try:
|
||||||
|
# Создаем необходимые директории
|
||||||
|
AppConfig.create_directories()
|
||||||
|
|
||||||
|
# Инициализируем логирование
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
# Загружаем настройки
|
||||||
|
self._settings = Settings()
|
||||||
|
self._settings.load()
|
||||||
|
|
||||||
|
# Создаем менеджеры
|
||||||
|
self._state = StateManager()
|
||||||
|
self._config_manager = ConfigManager()
|
||||||
|
self._config_watcher = ConfigWatcher()
|
||||||
|
self._serial = SerialManager()
|
||||||
|
self._command_handler = CommandHandler(self._serial)
|
||||||
|
self._tftp = TFTPServer()
|
||||||
|
self._transfer = TransferManager(self._serial, self._tftp)
|
||||||
|
|
||||||
|
self._logger.info("Компоненты приложения инициализированы")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка инициализации компонентов: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _setup_event_handlers(self) -> None:
|
||||||
|
"""Настройка обработчиков событий."""
|
||||||
|
# Обработка изменения настроек
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.UI_SETTINGS_CHANGED,
|
||||||
|
self._handle_settings_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обработка событий подключения
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.CONNECTION_ESTABLISHED,
|
||||||
|
self._handle_connection_established
|
||||||
|
)
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.CONNECTION_LOST,
|
||||||
|
self._handle_connection_lost
|
||||||
|
)
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.CONNECTION_ERROR,
|
||||||
|
self._handle_connection_error
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обработка событий передачи
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.TRANSFER_COMPLETED,
|
||||||
|
self._handle_transfer_completed
|
||||||
|
)
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.TRANSFER_ERROR,
|
||||||
|
self._handle_transfer_error
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обработка событий конфигурации
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.CONFIG_MODIFIED,
|
||||||
|
self._handle_config_modified
|
||||||
|
)
|
||||||
|
event_bus.subscribe(
|
||||||
|
EventTypes.CONFIG_DELETED,
|
||||||
|
self._handle_config_deleted
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_settings_changed(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка изменения настроек.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие изменения настроек
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
settings = event.data
|
||||||
|
|
||||||
|
# Применяем настройки к компонентам
|
||||||
|
self._serial.configure(settings)
|
||||||
|
self._tftp.configure(
|
||||||
|
port=settings.get("tftp_port"),
|
||||||
|
root_dir=AppConfig.CONFIGS_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем настройки
|
||||||
|
self._settings.update(settings)
|
||||||
|
|
||||||
|
self._logger.info("Настройки обновлены")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка применения настроек: {e}")
|
||||||
|
|
||||||
|
def _handle_connection_established(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка установки соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие установки соединения
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Получаем информацию об устройстве
|
||||||
|
device_info = self._command_handler.get_device_info()
|
||||||
|
|
||||||
|
# Обновляем состояние
|
||||||
|
self._state.update_device_info(device_info)
|
||||||
|
|
||||||
|
self._logger.info("Соединение установлено")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка получения информации об устройстве: {e}")
|
||||||
|
|
||||||
|
def _handle_connection_lost(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка потери соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие потери соединения
|
||||||
|
"""
|
||||||
|
# Отменяем все активные передачи
|
||||||
|
self._transfer.cancel_all_transfers()
|
||||||
|
|
||||||
|
# Обновляем состояние
|
||||||
|
self._state.clear_device_info()
|
||||||
|
|
||||||
|
self._logger.info("Соединение потеряно")
|
||||||
|
|
||||||
|
def _handle_connection_error(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка ошибки соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие ошибки соединения
|
||||||
|
"""
|
||||||
|
error = event.data
|
||||||
|
self._logger.error(f"Ошибка соединения: {error}")
|
||||||
|
|
||||||
|
def _handle_transfer_completed(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка завершения передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие завершения передачи
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transfer_info = event.data
|
||||||
|
|
||||||
|
# Очищаем завершенные передачи
|
||||||
|
self._transfer.cleanup_transfers()
|
||||||
|
|
||||||
|
self._logger.info(f"Передача завершена: {transfer_info}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки завершения передачи: {e}")
|
||||||
|
|
||||||
|
def _handle_transfer_error(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка ошибки передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие ошибки передачи
|
||||||
|
"""
|
||||||
|
error = event.data
|
||||||
|
self._logger.error(f"Ошибка передачи: {error}")
|
||||||
|
|
||||||
|
def _handle_config_modified(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка изменения конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие изменения конфигурации
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
file_info = event.data
|
||||||
|
|
||||||
|
# Обновляем информацию о файле
|
||||||
|
self._state.update_config_info(file_info)
|
||||||
|
|
||||||
|
self._logger.info(f"Конфигурация изменена: {file_info['name']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки изменения конфигурации: {e}")
|
||||||
|
|
||||||
|
def _handle_config_deleted(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка удаления конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие удаления конфигурации
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
file_info = event.data
|
||||||
|
|
||||||
|
# Удаляем информацию о файле
|
||||||
|
self._state.remove_config_info(file_info["name"])
|
||||||
|
|
||||||
|
# Удаляем файл из отслеживания
|
||||||
|
self._config_watcher.remove_watch(file_info["name"])
|
||||||
|
|
||||||
|
self._logger.info(f"Конфигурация удалена: {file_info['name']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки удаления конфигурации: {e}")
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Запуск приложения."""
|
||||||
|
try:
|
||||||
|
# Запускаем наблюдатель за конфигурациями
|
||||||
|
self._config_watcher.start()
|
||||||
|
self._config_watcher.watch_all_configs()
|
||||||
|
|
||||||
|
# Создаем и запускаем главное окно
|
||||||
|
self._window = MainWindow(
|
||||||
|
self._settings,
|
||||||
|
self._state,
|
||||||
|
self._serial,
|
||||||
|
self._transfer,
|
||||||
|
self._config_manager
|
||||||
|
)
|
||||||
|
self._window.mainloop()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка запуска приложения: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""Очистка ресурсов приложения."""
|
||||||
|
try:
|
||||||
|
# Останавливаем компоненты
|
||||||
|
if self._serial.is_connected:
|
||||||
|
self._serial.disconnect()
|
||||||
|
|
||||||
|
if self._tftp.is_running:
|
||||||
|
self._tftp.stop()
|
||||||
|
|
||||||
|
if self._config_watcher.is_running:
|
||||||
|
self._config_watcher.stop()
|
||||||
|
|
||||||
|
# Отменяем все передачи
|
||||||
|
self._transfer.cancel_all_transfers()
|
||||||
|
|
||||||
|
# Сохраняем настройки
|
||||||
|
self._settings.save()
|
||||||
|
|
||||||
|
self._logger.info("Ресурсы приложения освобождены")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка очистки ресурсов: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Точка входа в приложение."""
|
||||||
|
app = App()
|
||||||
|
app.start()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
102
src/core/config.py
Normal file
102
src/core/config.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
class AppConfig:
|
||||||
|
"""Конфигурация приложения."""
|
||||||
|
|
||||||
|
# Версия приложения
|
||||||
|
VERSION: str = "1.0.0"
|
||||||
|
|
||||||
|
# Директории
|
||||||
|
LOGS_DIR: str = "Logs"
|
||||||
|
CONFIGS_DIR: str = "Configs"
|
||||||
|
SETTINGS_DIR: str = "Settings"
|
||||||
|
FIRMWARE_DIR: str = "Firmware"
|
||||||
|
DOCS_DIR: str = "docs"
|
||||||
|
|
||||||
|
# Файлы
|
||||||
|
LOG_FILE: str = "app.log"
|
||||||
|
SETTINGS_FILE: str = "settings.json"
|
||||||
|
|
||||||
|
# Настройки логирования
|
||||||
|
LOG_MAX_BYTES: int = 5 * 1024 * 1024 # 5 MB
|
||||||
|
LOG_BACKUP_COUNT: int = 3
|
||||||
|
|
||||||
|
# Настройки TFTP
|
||||||
|
TFTP_PORT: int = 69
|
||||||
|
TFTP_TIMEOUT: int = 5
|
||||||
|
TFTP_RETRIES: int = 3
|
||||||
|
|
||||||
|
# Настройки последовательного порта по умолчанию
|
||||||
|
DEFAULT_BAUDRATE: int = 9600
|
||||||
|
DEFAULT_TIMEOUT: int = 10
|
||||||
|
DEFAULT_PROMPT: str = ">"
|
||||||
|
|
||||||
|
# Настройки передачи конфигурации
|
||||||
|
DEFAULT_BLOCK_SIZE: int = 15
|
||||||
|
DEFAULT_COPY_MODE: str = "line"
|
||||||
|
|
||||||
|
# Поддерживаемые скорости передачи
|
||||||
|
SUPPORTED_BAUDRATES: List[int] = [
|
||||||
|
1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
|
||||||
|
]
|
||||||
|
|
||||||
|
# Поддерживаемые режимы копирования
|
||||||
|
SUPPORTED_COPY_MODES: List[str] = [
|
||||||
|
"line", # Построчный режим
|
||||||
|
"block", # Блочный режим
|
||||||
|
"tftp" # Через TFTP
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_directories(cls) -> None:
|
||||||
|
"""Создание необходимых директорий."""
|
||||||
|
directories = [
|
||||||
|
cls.LOGS_DIR,
|
||||||
|
cls.CONFIGS_DIR,
|
||||||
|
cls.SETTINGS_DIR,
|
||||||
|
cls.FIRMWARE_DIR,
|
||||||
|
cls.DOCS_DIR
|
||||||
|
]
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_log_path(cls) -> str:
|
||||||
|
"""Получение пути к файлу лога."""
|
||||||
|
return os.path.join(cls.LOGS_DIR, cls.LOG_FILE)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_settings_path(cls) -> str:
|
||||||
|
"""Получение пути к файлу настроек."""
|
||||||
|
return os.path.join(cls.SETTINGS_DIR, cls.SETTINGS_FILE)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_path(cls, config_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Получение пути к файлу конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Полный путь к файлу конфигурации
|
||||||
|
"""
|
||||||
|
return os.path.join(cls.CONFIGS_DIR, config_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_firmware_path(cls, firmware_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Получение пути к файлу прошивки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
firmware_name: Имя файла прошивки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Полный путь к файлу прошивки
|
||||||
|
"""
|
||||||
|
return os.path.join(cls.FIRMWARE_DIR, firmware_name)
|
||||||
90
src/core/events.py
Normal file
90
src/core/events.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Event:
|
||||||
|
"""Базовый класс для событий."""
|
||||||
|
type: str
|
||||||
|
data: Any
|
||||||
|
timestamp: datetime = datetime.now()
|
||||||
|
|
||||||
|
class EventBus:
|
||||||
|
"""Шина событий для асинхронного взаимодействия компонентов."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._subscribers: Dict[str, List[Callable[[Event], None]]] = {}
|
||||||
|
|
||||||
|
def subscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
|
||||||
|
"""
|
||||||
|
Подписка на событие.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type: Тип события
|
||||||
|
callback: Функция обратного вызова
|
||||||
|
"""
|
||||||
|
if event_type not in self._subscribers:
|
||||||
|
self._subscribers[event_type] = []
|
||||||
|
self._subscribers[event_type].append(callback)
|
||||||
|
|
||||||
|
def unsubscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
|
||||||
|
"""
|
||||||
|
Отписка от события.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type: Тип события
|
||||||
|
callback: Функция обратного вызова
|
||||||
|
"""
|
||||||
|
if event_type in self._subscribers:
|
||||||
|
self._subscribers[event_type].remove(callback)
|
||||||
|
|
||||||
|
def publish(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Публикация события.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Объект события
|
||||||
|
"""
|
||||||
|
if event.type in self._subscribers:
|
||||||
|
for callback in self._subscribers[event.type]:
|
||||||
|
callback(event)
|
||||||
|
|
||||||
|
# Создаем глобальный экземпляр шины событий
|
||||||
|
event_bus = EventBus()
|
||||||
|
|
||||||
|
# Определяем типы событий
|
||||||
|
class EventTypes:
|
||||||
|
"""Константы для типов событий."""
|
||||||
|
|
||||||
|
# События подключения
|
||||||
|
CONNECTION_ESTABLISHED = "connection_established"
|
||||||
|
CONNECTION_LOST = "connection_lost"
|
||||||
|
CONNECTION_ERROR = "connection_error"
|
||||||
|
|
||||||
|
# События передачи данных
|
||||||
|
TRANSFER_STARTED = "transfer_started"
|
||||||
|
TRANSFER_PROGRESS = "transfer_progress"
|
||||||
|
TRANSFER_COMPLETED = "transfer_completed"
|
||||||
|
TRANSFER_ERROR = "transfer_error"
|
||||||
|
|
||||||
|
# События конфигурации
|
||||||
|
CONFIG_LOADED = "config_loaded"
|
||||||
|
CONFIG_SAVED = "config_saved"
|
||||||
|
CONFIG_ERROR = "config_error"
|
||||||
|
CONFIG_MODIFIED = "config_modified"
|
||||||
|
CONFIG_DELETED = "config_deleted"
|
||||||
|
CONFIG_CREATED = "config_created"
|
||||||
|
|
||||||
|
# События TFTP
|
||||||
|
TFTP_SERVER_STARTED = "tftp_server_started"
|
||||||
|
TFTP_SERVER_STOPPED = "tftp_server_stopped"
|
||||||
|
TFTP_TRANSFER_STARTED = "tftp_transfer_started"
|
||||||
|
TFTP_TRANSFER_COMPLETED = "tftp_transfer_completed"
|
||||||
|
TFTP_ERROR = "tftp_error"
|
||||||
|
|
||||||
|
# События UI
|
||||||
|
UI_SETTINGS_CHANGED = "ui_settings_changed"
|
||||||
|
UI_STATUS_CHANGED = "ui_status_changed"
|
||||||
38
src/core/exceptions.py
Normal file
38
src/core/exceptions.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
class ComConfigError(Exception):
|
||||||
|
"""Базовый класс для всех исключений приложения."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConnectionError(ComConfigError):
|
||||||
|
"""Ошибка подключения к устройству."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuthenticationError(ConnectionError):
|
||||||
|
"""Ошибка аутентификации."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConfigError(ComConfigError):
|
||||||
|
"""Ошибка работы с конфигурацией."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TransferError(ComConfigError):
|
||||||
|
"""Ошибка передачи данных."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TFTPError(ComConfigError):
|
||||||
|
"""Ошибка TFTP сервера."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ValidationError(ComConfigError):
|
||||||
|
"""Ошибка валидации данных."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SettingsError(ComConfigError):
|
||||||
|
"""Ошибка работы с настройками."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FileSystemError(ComConfigError):
|
||||||
|
"""Ошибка работы с файловой системой."""
|
||||||
|
pass
|
||||||
412
src/core/state.py
Normal file
412
src/core/state.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional, List
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum, auto
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from .events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
class ConnectionState(Enum):
|
||||||
|
"""Состояния подключения."""
|
||||||
|
DISCONNECTED = auto()
|
||||||
|
CONNECTING = auto()
|
||||||
|
CONNECTED = auto()
|
||||||
|
ERROR = auto()
|
||||||
|
|
||||||
|
class TransferState(Enum):
|
||||||
|
"""Состояния передачи данных."""
|
||||||
|
IDLE = auto()
|
||||||
|
PREPARING = auto()
|
||||||
|
TRANSFERRING = auto()
|
||||||
|
COMPLETED = auto()
|
||||||
|
ERROR = auto()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceInfo:
|
||||||
|
"""Информация об устройстве."""
|
||||||
|
version: Optional[str] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
hostname: Optional[str] = None
|
||||||
|
interfaces: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
last_update: Optional[datetime] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConfigInfo:
|
||||||
|
"""Информация о файле конфигурации."""
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
size: int
|
||||||
|
modified: datetime
|
||||||
|
created: datetime
|
||||||
|
is_watched: bool = False
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApplicationState:
|
||||||
|
"""Состояние приложения."""
|
||||||
|
connection_state: ConnectionState = ConnectionState.DISCONNECTED
|
||||||
|
transfer_state: TransferState = TransferState.IDLE
|
||||||
|
device_info: DeviceInfo = field(default_factory=DeviceInfo)
|
||||||
|
configs: Dict[str, ConfigInfo] = field(default_factory=dict)
|
||||||
|
current_config: Optional[str] = None
|
||||||
|
transfer_progress: float = 0.0
|
||||||
|
status_message: str = ""
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
is_tftp_server_running: bool = False
|
||||||
|
custom_data: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
class StateManager:
|
||||||
|
"""Менеджер состояния приложения."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._state = ApplicationState()
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._setup_event_handlers()
|
||||||
|
|
||||||
|
def _setup_event_handlers(self) -> None:
|
||||||
|
"""Настройка обработчиков событий."""
|
||||||
|
event_bus.subscribe(EventTypes.CONNECTION_ESTABLISHED, self._handle_connection_established)
|
||||||
|
event_bus.subscribe(EventTypes.CONNECTION_LOST, self._handle_connection_lost)
|
||||||
|
event_bus.subscribe(EventTypes.CONNECTION_ERROR, self._handle_connection_error)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_STARTED, self._handle_transfer_started)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_PROGRESS, self._handle_transfer_progress)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_COMPLETED, self._handle_transfer_completed)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_ERROR, self._handle_transfer_error)
|
||||||
|
event_bus.subscribe(EventTypes.TFTP_SERVER_STARTED, self._handle_tftp_server_started)
|
||||||
|
event_bus.subscribe(EventTypes.TFTP_SERVER_STOPPED, self._handle_tftp_server_stopped)
|
||||||
|
|
||||||
|
# Подписка на события конфигурации
|
||||||
|
event_bus.subscribe(EventTypes.CONFIG_CREATED, self._handle_config_created)
|
||||||
|
event_bus.subscribe(EventTypes.CONFIG_MODIFIED, self._handle_config_modified)
|
||||||
|
event_bus.subscribe(EventTypes.CONFIG_DELETED, self._handle_config_deleted)
|
||||||
|
|
||||||
|
def _handle_connection_established(self, event: Event) -> None:
|
||||||
|
self._state.connection_state = ConnectionState.CONNECTED
|
||||||
|
self._state.error_message = None
|
||||||
|
self._update_status("Подключение установлено")
|
||||||
|
|
||||||
|
def _handle_connection_lost(self, event: Event) -> None:
|
||||||
|
self._state.connection_state = ConnectionState.DISCONNECTED
|
||||||
|
self._update_status("Подключение потеряно")
|
||||||
|
|
||||||
|
def _handle_connection_error(self, event: Event) -> None:
|
||||||
|
self._state.connection_state = ConnectionState.ERROR
|
||||||
|
self._state.error_message = str(event.data)
|
||||||
|
self._update_status(f"Ошибка подключения: {event.data}")
|
||||||
|
|
||||||
|
def _handle_transfer_started(self, event: Event) -> None:
|
||||||
|
self._state.transfer_state = TransferState.TRANSFERRING
|
||||||
|
self._state.transfer_progress = 0.0
|
||||||
|
self._update_status("Передача данных начата")
|
||||||
|
|
||||||
|
def _handle_transfer_progress(self, event: Event) -> None:
|
||||||
|
self._state.transfer_progress = float(event.data)
|
||||||
|
self._update_status(f"Прогресс передачи: {self._state.transfer_progress:.1f}%")
|
||||||
|
|
||||||
|
def _handle_transfer_completed(self, event: Event) -> None:
|
||||||
|
self._state.transfer_state = TransferState.COMPLETED
|
||||||
|
self._state.transfer_progress = 100.0
|
||||||
|
self._update_status("Передача данных завершена")
|
||||||
|
|
||||||
|
def _handle_transfer_error(self, event: Event) -> None:
|
||||||
|
self._state.transfer_state = TransferState.ERROR
|
||||||
|
self._state.error_message = str(event.data)
|
||||||
|
self._update_status(f"Ошибка передачи: {event.data}")
|
||||||
|
|
||||||
|
def _handle_tftp_server_started(self, event: Event) -> None:
|
||||||
|
self._state.is_tftp_server_running = True
|
||||||
|
self._update_status("TFTP сервер запущен")
|
||||||
|
|
||||||
|
def _handle_tftp_server_stopped(self, event: Event) -> None:
|
||||||
|
self._state.is_tftp_server_running = False
|
||||||
|
self._update_status("TFTP сервер остановлен")
|
||||||
|
|
||||||
|
def _handle_config_created(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка создания конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие создания конфигурации
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.update_config_info(event.data)
|
||||||
|
self._update_status(f"Конфигурация создана: {event.data['name']}")
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки создания конфигурации: {e}")
|
||||||
|
|
||||||
|
def _handle_config_modified(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка изменения конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие изменения конфигурации
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.update_config_info(event.data)
|
||||||
|
self._update_status(f"Конфигурация изменена: {event.data['name']}")
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки изменения конфигурации: {e}")
|
||||||
|
|
||||||
|
def _handle_config_deleted(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка удаления конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие удаления конфигурации
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_name = event.data['name']
|
||||||
|
self.remove_config_info(config_name)
|
||||||
|
self._update_status(f"Конфигурация удалена: {config_name}")
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки удаления конфигурации: {e}")
|
||||||
|
|
||||||
|
def _update_status(self, message: str) -> None:
|
||||||
|
"""Обновление статусного сообщения."""
|
||||||
|
self._state.status_message = message
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, message))
|
||||||
|
|
||||||
|
def update_device_info(self, info: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Обновление информации об устройстве.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
info: Словарь с информацией об устройстве
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._state.device_info = DeviceInfo(
|
||||||
|
version=info.get("version"),
|
||||||
|
model=info.get("model"),
|
||||||
|
hostname=info.get("hostname"),
|
||||||
|
interfaces=info.get("interfaces", []),
|
||||||
|
last_update=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем состояние подключения
|
||||||
|
self._state.connection_state = ConnectionState.CONNECTED
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": f"Подключено к {self._state.device_info.hostname}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info("Информация об устройстве обновлена")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обновления информации об устройстве: {e}")
|
||||||
|
self._state.connection_state = ConnectionState.ERROR
|
||||||
|
self._state.error_message = str(e)
|
||||||
|
|
||||||
|
def clear_device_info(self) -> None:
|
||||||
|
"""Очистка информации об устройстве."""
|
||||||
|
self._state.device_info = DeviceInfo()
|
||||||
|
self._state.connection_state = ConnectionState.DISCONNECTED
|
||||||
|
self._state.error_message = None
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": "Отключено от устройства"
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info("Информация об устройстве очищена")
|
||||||
|
|
||||||
|
def update_config_info(self, info: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Обновление информации о файле конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
info: Словарь с информацией о файле
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config = ConfigInfo(
|
||||||
|
name=info["name"],
|
||||||
|
path=info["path"],
|
||||||
|
size=info["size"],
|
||||||
|
modified=info["modified"],
|
||||||
|
created=info["created"],
|
||||||
|
is_watched=info.get("is_watched", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._state.configs[config.name] = config
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": f"Конфигурация обновлена: {config.name}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.debug(f"Обновлена информация о конфигурации: {config.name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обновления информации о конфигурации: {e}")
|
||||||
|
|
||||||
|
def remove_config_info(self, config_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Удаление информации о файле конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
"""
|
||||||
|
if config_name in self._state.configs:
|
||||||
|
self._state.configs.pop(config_name)
|
||||||
|
|
||||||
|
# Если удаляется текущая конфигурация, очищаем её
|
||||||
|
if self._state.current_config == config_name:
|
||||||
|
self._state.current_config = None
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": f"Конфигурация удалена: {config_name}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.debug(f"Удалена информация о конфигурации: {config_name}")
|
||||||
|
|
||||||
|
def set_current_config(self, config_name: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Установка текущей конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
"""
|
||||||
|
if config_name is None or config_name in self._state.configs:
|
||||||
|
self._state.current_config = config_name
|
||||||
|
|
||||||
|
if config_name:
|
||||||
|
self._logger.debug(f"Установлена текущая конфигурация: {config_name}")
|
||||||
|
else:
|
||||||
|
self._logger.debug("Текущая конфигурация очищена")
|
||||||
|
|
||||||
|
def update_transfer_state(self, state: TransferState, progress: float = 0.0,
|
||||||
|
error: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Обновление состояния передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: Новое состояние
|
||||||
|
progress: Прогресс передачи
|
||||||
|
error: Сообщение об ошибке
|
||||||
|
"""
|
||||||
|
self._state.transfer_state = state
|
||||||
|
self._state.transfer_progress = progress
|
||||||
|
self._state.error_message = error
|
||||||
|
|
||||||
|
# Формируем сообщение о состоянии
|
||||||
|
if state == TransferState.IDLE:
|
||||||
|
message = "Готов к передаче"
|
||||||
|
elif state == TransferState.PREPARING:
|
||||||
|
message = "Подготовка к передаче..."
|
||||||
|
elif state == TransferState.TRANSFERRING:
|
||||||
|
message = f"Передача данных: {progress:.1f}%"
|
||||||
|
elif state == TransferState.COMPLETED:
|
||||||
|
message = "Передача завершена"
|
||||||
|
elif state == TransferState.ERROR:
|
||||||
|
message = f"Ошибка передачи: {error}"
|
||||||
|
else:
|
||||||
|
message = "Неизвестное состояние"
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": message
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.debug(f"Состояние передачи обновлено: {state.name}")
|
||||||
|
|
||||||
|
def set_tftp_server_state(self, is_running: bool) -> None:
|
||||||
|
"""
|
||||||
|
Установка состояния TFTP сервера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
is_running: Флаг работы сервера
|
||||||
|
"""
|
||||||
|
self._state.is_tftp_server_running = is_running
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": "TFTP сервер запущен" if is_running else "TFTP сервер остановлен"
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.debug(f"Состояние TFTP сервера: {'запущен' if is_running else 'остановлен'}")
|
||||||
|
|
||||||
|
def set_status_message(self, message: str) -> None:
|
||||||
|
"""
|
||||||
|
Установка сообщения о состоянии.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Сообщение
|
||||||
|
"""
|
||||||
|
self._state.status_message = message
|
||||||
|
|
||||||
|
# Уведомляем об изменении состояния
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": message
|
||||||
|
}))
|
||||||
|
|
||||||
|
def set_error_message(self, error: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Установка сообщения об ошибке.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error: Сообщение об ошибке
|
||||||
|
"""
|
||||||
|
self._state.error_message = error
|
||||||
|
|
||||||
|
if error:
|
||||||
|
# Уведомляем об ошибке
|
||||||
|
event_bus.publish(Event(EventTypes.UI_STATUS_CHANGED, {
|
||||||
|
"message": f"Ошибка: {error}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
def get_device_info(self) -> DeviceInfo:
|
||||||
|
"""
|
||||||
|
Получение информации об устройстве.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DeviceInfo: Информация об устройстве
|
||||||
|
"""
|
||||||
|
return self._state.device_info
|
||||||
|
|
||||||
|
def get_config_info(self, config_name: str) -> Optional[ConfigInfo]:
|
||||||
|
"""
|
||||||
|
Получение информации о конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[ConfigInfo]: Информация о конфигурации или None
|
||||||
|
"""
|
||||||
|
return self._state.configs.get(config_name)
|
||||||
|
|
||||||
|
def get_configs(self) -> Dict[str, ConfigInfo]:
|
||||||
|
"""
|
||||||
|
Получение списка всех конфигураций.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, ConfigInfo]: Словарь с информацией о конфигурациях
|
||||||
|
"""
|
||||||
|
return self._state.configs.copy()
|
||||||
|
|
||||||
|
def get_current_config(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получение текущей конфигурации.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Имя текущей конфигурации или None
|
||||||
|
"""
|
||||||
|
return self._state.current_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> ApplicationState:
|
||||||
|
"""Получение текущего состояния."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def set_custom_data(self, key: str, value: Any) -> None:
|
||||||
|
"""Установка пользовательских данных."""
|
||||||
|
self._state.custom_data[key] = value
|
||||||
|
|
||||||
|
def get_custom_data(self, key: str, default: Any = None) -> Any:
|
||||||
|
"""Получение пользовательских данных."""
|
||||||
|
return self._state.custom_data.get(key, default)
|
||||||
|
|
||||||
|
# Создаем глобальный экземпляр менеджера состояния
|
||||||
|
state_manager = StateManager()
|
||||||
0
src/filesystem/__init__.py
Normal file
0
src/filesystem/__init__.py
Normal file
211
src/filesystem/config_manager.py
Normal file
211
src/filesystem/config_manager.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.exceptions import FileSystemError
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
"""Менеджер для работы с файлами конфигураций."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._config_dir = AppConfig.CONFIGS_DIR
|
||||||
|
|
||||||
|
# Создаем директорию, если её нет
|
||||||
|
os.makedirs(self._config_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def save_config(self, config_name: str, content: str,
|
||||||
|
make_backup: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Сохранение конфигурации в файл.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
content: Содержимое конфигурации
|
||||||
|
make_backup: Создавать ли резервную копию
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Путь к сохраненному файлу
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке сохранения
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Формируем путь к файлу
|
||||||
|
config_path = os.path.join(self._config_dir, config_name)
|
||||||
|
|
||||||
|
# Создаем резервную копию, если файл существует
|
||||||
|
if make_backup and os.path.exists(config_path):
|
||||||
|
self._create_backup(config_path)
|
||||||
|
|
||||||
|
# Сохраняем конфигурацию
|
||||||
|
with open(config_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.CONFIG_SAVED, {
|
||||||
|
"file": config_path,
|
||||||
|
"size": len(content)
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info(f"Конфигурация сохранена: {config_name}")
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка сохранения конфигурации: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка сохранения конфигурации: {e}")
|
||||||
|
|
||||||
|
def load_config(self, config_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Загрузка конфигурации из файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Содержимое конфигурации
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке загрузки
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_path = os.path.join(self._config_dir, config_name)
|
||||||
|
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
raise FileSystemError(f"Файл не найден: {config_name}")
|
||||||
|
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.CONFIG_LOADED, {
|
||||||
|
"file": config_path,
|
||||||
|
"size": len(content)
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info(f"Конфигурация загружена: {config_name}")
|
||||||
|
return content
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка загрузки конфигурации: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка загрузки конфигурации: {e}")
|
||||||
|
|
||||||
|
def delete_config(self, config_name: str, keep_backup: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Удаление файла конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
keep_backup: Сохранить ли резервную копию
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке удаления
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_path = os.path.join(self._config_dir, config_name)
|
||||||
|
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Создаем резервную копию перед удалением
|
||||||
|
if keep_backup:
|
||||||
|
self._create_backup(config_path)
|
||||||
|
|
||||||
|
# Удаляем файл
|
||||||
|
os.remove(config_path)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.CONFIG_DELETED, {
|
||||||
|
"file": config_path
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info(f"Конфигурация удалена: {config_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка удаления конфигурации: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка удаления конфигурации: {e}")
|
||||||
|
|
||||||
|
def list_configs(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Получение списка файлов конфигураций.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: Список с информацией о файлах
|
||||||
|
"""
|
||||||
|
configs = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for filename in os.listdir(self._config_dir):
|
||||||
|
file_path = os.path.join(self._config_dir, filename)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
stat = os.stat(file_path)
|
||||||
|
configs.append({
|
||||||
|
"name": filename,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"modified": datetime.fromtimestamp(stat.st_mtime),
|
||||||
|
"created": datetime.fromtimestamp(stat.st_ctime)
|
||||||
|
})
|
||||||
|
|
||||||
|
return sorted(configs, key=lambda x: x["modified"], reverse=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка получения списка конфигураций: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_config_info(self, config_name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Получение информации о файле конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_name: Имя файла конфигурации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Dict[str, Any]]: Информация о файле или None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_path = os.path.join(self._config_dir, config_name)
|
||||||
|
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
stat = os.stat(config_path)
|
||||||
|
return {
|
||||||
|
"name": config_name,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"modified": datetime.fromtimestamp(stat.st_mtime),
|
||||||
|
"created": datetime.fromtimestamp(stat.st_ctime),
|
||||||
|
"path": config_path
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка получения информации о конфигурации: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _create_backup(self, file_path: str) -> None:
|
||||||
|
"""
|
||||||
|
Создание резервной копии файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Путь к файлу
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке создания копии
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Формируем имя для резервной копии
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_name = f"{os.path.basename(file_path)}.{timestamp}.bak"
|
||||||
|
backup_path = os.path.join(self._config_dir, backup_name)
|
||||||
|
|
||||||
|
# Копируем файл
|
||||||
|
shutil.copy2(file_path, backup_path)
|
||||||
|
|
||||||
|
self._logger.info(f"Создана резервная копия: {backup_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка создания резервной копии: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка создания резервной копии: {e}")
|
||||||
70
src/filesystem/logger.py
Normal file
70
src/filesystem/logger.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def setup_logging(
|
||||||
|
log_dir: str = "Logs",
|
||||||
|
log_file: str = "app.log",
|
||||||
|
max_bytes: int = 5 * 1024 * 1024, # 5 MB
|
||||||
|
backup_count: int = 3,
|
||||||
|
log_level: int = logging.DEBUG,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Настройка системы логирования.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_dir: Директория для хранения логов
|
||||||
|
log_file: Имя файла лога
|
||||||
|
max_bytes: Максимальный размер файла лога
|
||||||
|
backup_count: Количество файлов ротации
|
||||||
|
log_level: Уровень логирования
|
||||||
|
"""
|
||||||
|
# Создаем директорию для логов, если её нет
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Настраиваем корневой логгер
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(log_level)
|
||||||
|
|
||||||
|
# Очищаем существующие обработчики
|
||||||
|
logger.handlers = []
|
||||||
|
|
||||||
|
# Создаем форматтер
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
"%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем обработчик файла с ротацией
|
||||||
|
log_path = os.path.join(log_dir, log_file)
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
log_path,
|
||||||
|
maxBytes=max_bytes,
|
||||||
|
backupCount=backup_count,
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Добавляем обработчик консоли для отладки
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
logging.info("Система логирования инициализирована")
|
||||||
|
|
||||||
|
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Получение логгера с указанным именем.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя логгера
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logging.Logger: Настроенный логгер
|
||||||
|
"""
|
||||||
|
return logging.getLogger(name)
|
||||||
78
src/filesystem/settings.py
Normal file
78
src/filesystem/settings.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""Класс для работы с настройками приложения."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.settings_dir = "Settings"
|
||||||
|
self.settings_file = os.path.join(self.settings_dir, "settings.json")
|
||||||
|
self._settings = self._get_default_settings()
|
||||||
|
|
||||||
|
# Создаем директорию для настроек, если её нет
|
||||||
|
os.makedirs(self.settings_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def _get_default_settings(self) -> Dict[str, Any]:
|
||||||
|
"""Возвращает настройки по умолчанию."""
|
||||||
|
return {
|
||||||
|
"port": None, # Порт для подключения
|
||||||
|
"baudrate": 9600, # Скорость передачи данных
|
||||||
|
"config_file": None, # Файл конфигурации
|
||||||
|
"login": None, # Логин для подключения
|
||||||
|
"password": None, # Пароль для подключения
|
||||||
|
"timeout": 10, # Таймаут подключения
|
||||||
|
"copy_mode": "line", # Режим копирования
|
||||||
|
"block_size": 15, # Размер блока команд
|
||||||
|
"prompt": ">", # Используется для определения приглашения
|
||||||
|
}
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
"""Загружает настройки из файла."""
|
||||||
|
if not os.path.exists(self.settings_file):
|
||||||
|
self.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.settings_file, "r", encoding="utf-8") as f:
|
||||||
|
loaded_settings = json.load(f)
|
||||||
|
|
||||||
|
# Проверяем наличие всех необходимых параметров
|
||||||
|
default_settings = self._get_default_settings()
|
||||||
|
for key, value in default_settings.items():
|
||||||
|
if key not in loaded_settings:
|
||||||
|
loaded_settings[key] = value
|
||||||
|
|
||||||
|
self._settings = loaded_settings
|
||||||
|
self.save() # Сохраняем обновленные настройки
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка при загрузке настроек: {e}", exc_info=True)
|
||||||
|
self._settings = self._get_default_settings()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
"""Сохраняет настройки в файл."""
|
||||||
|
try:
|
||||||
|
with open(self.settings_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self._settings, f, indent=4, ensure_ascii=False)
|
||||||
|
logging.info("Настройки успешно сохранены")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка при сохранении настроек: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
|
"""Возвращает значение настройки по ключу."""
|
||||||
|
return self._settings.get(key, default)
|
||||||
|
|
||||||
|
def set(self, key: str, value: Any) -> None:
|
||||||
|
"""Устанавливает значение настройки."""
|
||||||
|
self._settings[key] = value
|
||||||
|
|
||||||
|
def update(self, settings: Dict[str, Any]) -> None:
|
||||||
|
"""Обновляет несколько настроек одновременно."""
|
||||||
|
self._settings.update(settings)
|
||||||
|
self.save()
|
||||||
247
src/filesystem/watchers/config_watcher.py
Normal file
247
src/filesystem/watchers/config_watcher.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Optional, Dict, Set, Callable, Any
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.exceptions import FileSystemError
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
class ConfigEventHandler(FileSystemEventHandler):
|
||||||
|
"""Обработчик событий файловой системы для конфигураций."""
|
||||||
|
|
||||||
|
def __init__(self, callback: Callable[[str, str], None]):
|
||||||
|
self._callback = callback
|
||||||
|
|
||||||
|
def on_created(self, event: FileSystemEvent) -> None:
|
||||||
|
"""
|
||||||
|
Обработка создания файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие файловой системы
|
||||||
|
"""
|
||||||
|
if not event.is_directory:
|
||||||
|
self._callback(event.src_path, "created")
|
||||||
|
|
||||||
|
def on_modified(self, event: FileSystemEvent) -> None:
|
||||||
|
"""
|
||||||
|
Обработка изменения файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие файловой системы
|
||||||
|
"""
|
||||||
|
if not event.is_directory:
|
||||||
|
self._callback(event.src_path, "modified")
|
||||||
|
|
||||||
|
def on_deleted(self, event: FileSystemEvent) -> None:
|
||||||
|
"""
|
||||||
|
Обработка удаления файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие файловой системы
|
||||||
|
"""
|
||||||
|
if not event.is_directory:
|
||||||
|
self._callback(event.src_path, "deleted")
|
||||||
|
|
||||||
|
class ConfigWatcher:
|
||||||
|
"""Наблюдатель за изменениями файлов конфигурации."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._observer: Optional[Observer] = None
|
||||||
|
self._watched_files: Set[str] = set()
|
||||||
|
self._config_dir = AppConfig.CONFIGS_DIR
|
||||||
|
|
||||||
|
# Создаем директорию, если её нет
|
||||||
|
os.makedirs(self._config_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def get_file_info(self, filename: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Получение информации о файле.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Dict[str, Any]]: Информация о файле или None
|
||||||
|
"""
|
||||||
|
file_path = os.path.join(self._config_dir, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
stat = os.stat(file_path)
|
||||||
|
return {
|
||||||
|
"name": filename,
|
||||||
|
"path": file_path,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"modified": datetime.fromtimestamp(stat.st_mtime),
|
||||||
|
"created": datetime.fromtimestamp(stat.st_ctime),
|
||||||
|
"is_watched": file_path in self._watched_files
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка получения информации о файле: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
Запуск наблюдателя.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке запуска
|
||||||
|
"""
|
||||||
|
if self._observer is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._observer = Observer()
|
||||||
|
handler = ConfigEventHandler(self._handle_event)
|
||||||
|
|
||||||
|
# Начинаем наблюдение за директорией
|
||||||
|
self._observer.schedule(handler, self._config_dir, recursive=False)
|
||||||
|
self._observer.start()
|
||||||
|
|
||||||
|
self._logger.info(f"Наблюдатель запущен: {self._config_dir}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Неожиданная ошибка: {e}")
|
||||||
|
raise FileSystemError(f"Неожиданная ошибка: {e}")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""
|
||||||
|
Остановка наблюдателя.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке остановки
|
||||||
|
"""
|
||||||
|
if self._observer is not None:
|
||||||
|
try:
|
||||||
|
self._observer.stop()
|
||||||
|
self._observer.join()
|
||||||
|
self._observer = None
|
||||||
|
self._watched_files.clear()
|
||||||
|
|
||||||
|
self._logger.info("Наблюдатель остановлен")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка остановки наблюдателя: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка остановки наблюдателя: {e}")
|
||||||
|
|
||||||
|
def add_watch(self, filename: str) -> None:
|
||||||
|
"""
|
||||||
|
Добавление файла для отслеживания.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке добавления файла
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(self._config_dir, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileSystemError(f"Файл не найден: {filename}")
|
||||||
|
|
||||||
|
self._watched_files.add(file_path)
|
||||||
|
self._logger.debug(f"Добавлен файл для отслеживания: {filename}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка добавления файла для отслеживания: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка добавления файла для отслеживания: {e}")
|
||||||
|
|
||||||
|
def remove_watch(self, filename: str) -> None:
|
||||||
|
"""
|
||||||
|
Удаление файла из отслеживания.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке удаления файла
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(self._config_dir, filename)
|
||||||
|
self._watched_files.discard(file_path)
|
||||||
|
self._logger.debug(f"Удален файл из отслеживания: {filename}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка удаления файла из отслеживания: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка удаления файла из отслеживания: {e}")
|
||||||
|
|
||||||
|
def _handle_event(self, file_path: str, event_type: str) -> None:
|
||||||
|
"""
|
||||||
|
Обработка события файловой системы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Путь к файлу
|
||||||
|
event_type: Тип события
|
||||||
|
"""
|
||||||
|
# Проверяем, отслеживается ли файл
|
||||||
|
if file_path not in self._watched_files:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем информацию о файле
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
file_info = self.get_file_info(filename)
|
||||||
|
|
||||||
|
if not file_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event_type == "created":
|
||||||
|
event_bus.publish(Event(EventTypes.CONFIG_CREATED, file_info))
|
||||||
|
self._logger.info(f"Создан файл конфигурации: {filename}")
|
||||||
|
|
||||||
|
elif event_type == "modified":
|
||||||
|
event_bus.publish(Event(EventTypes.CONFIG_MODIFIED, file_info))
|
||||||
|
self._logger.info(f"Изменен файл конфигурации: {filename}")
|
||||||
|
|
||||||
|
elif event_type == "deleted":
|
||||||
|
event_bus.publish(Event(EventTypes.CONFIG_DELETED, file_info))
|
||||||
|
self._logger.info(f"Удален файл конфигурации: {filename}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка обработки события: {e}")
|
||||||
|
|
||||||
|
def watch_all_configs(self) -> None:
|
||||||
|
"""
|
||||||
|
Добавление всех существующих конфигураций для отслеживания.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileSystemError: При ошибке добавления файлов
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
for filename in os.listdir(self._config_dir):
|
||||||
|
file_path = os.path.join(self._config_dir, filename)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
self._watched_files.add(file_path)
|
||||||
|
|
||||||
|
self._logger.info(f"Добавлено {len(self._watched_files)} файлов для отслеживания")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка добавления файлов для отслеживания: {e}")
|
||||||
|
raise FileSystemError(f"Ошибка добавления файлов для отслеживания: {e}")
|
||||||
|
|
||||||
|
def clear_watches(self) -> None:
|
||||||
|
"""Удаление всех файлов из отслеживания."""
|
||||||
|
self._watched_files.clear()
|
||||||
|
self._logger.info("Очищен список отслеживаемых файлов")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Проверка состояния наблюдателя."""
|
||||||
|
return self._observer is not None and self._observer.is_alive()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watched_files(self) -> Set[str]:
|
||||||
|
"""Получение списка отслеживаемых файлов."""
|
||||||
|
return {os.path.basename(path) for path in self._watched_files}
|
||||||
0
src/network/servers/__init__.py
Normal file
0
src/network/servers/__init__.py
Normal file
92
src/network/servers/base_server.py
Normal file
92
src/network/servers/base_server.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import threading
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
class BaseServer(ABC):
|
||||||
|
"""Базовый класс для всех серверов."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._running = False
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._config: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Проверка состояния сервера."""
|
||||||
|
return self._running
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
Запуск сервера.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: При ошибке запуска
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Остановка сервера."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def configure(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Конфигурация сервера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Параметры конфигурации
|
||||||
|
"""
|
||||||
|
self._config.update(kwargs)
|
||||||
|
|
||||||
|
def _start_in_thread(self, target) -> None:
|
||||||
|
"""
|
||||||
|
Запуск сервера в отдельном потоке.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target: Функция для выполнения в потоке
|
||||||
|
"""
|
||||||
|
if self._running:
|
||||||
|
self._logger.warning("Сервер уже запущен")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._thread = threading.Thread(target=target)
|
||||||
|
self._thread.daemon = True
|
||||||
|
self._thread.start()
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
def _stop_thread(self) -> None:
|
||||||
|
"""Остановка потока сервера."""
|
||||||
|
self._running = False
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
self._thread.join()
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
def _notify_started(self, data: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Уведомление о запуске сервера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Дополнительные данные
|
||||||
|
"""
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_SERVER_STARTED, data))
|
||||||
|
|
||||||
|
def _notify_stopped(self) -> None:
|
||||||
|
"""Уведомление об остановке сервера."""
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_SERVER_STOPPED, None))
|
||||||
|
|
||||||
|
def _notify_error(self, error: Exception) -> None:
|
||||||
|
"""
|
||||||
|
Уведомление об ошибке сервера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error: Объект ошибки
|
||||||
|
"""
|
||||||
|
event_bus.publish(Event(EventTypes.TFTP_ERROR, str(error)))
|
||||||
180
src/network/servers/tftp_server.py
Normal file
180
src/network/servers/tftp_server.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
from typing import Optional, Dict, Any, Callable
|
||||||
|
import tftpy
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.exceptions import TFTPError
|
||||||
|
from .base_server import BaseServer
|
||||||
|
|
||||||
|
class TFTPServer(BaseServer):
|
||||||
|
"""TFTP сервер для передачи файлов."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._server: Optional[tftpy.TftpServer] = None
|
||||||
|
self._root_dir = AppConfig.CONFIGS_DIR
|
||||||
|
self._host = "0.0.0.0"
|
||||||
|
self._port = AppConfig.TFTP_PORT
|
||||||
|
self._timeout = AppConfig.TFTP_TIMEOUT
|
||||||
|
|
||||||
|
# Создаем директорию, если её нет
|
||||||
|
os.makedirs(self._root_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def configure(self, root_dir: Optional[str] = None, host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None, timeout: Optional[int] = None) -> None:
|
||||||
|
"""
|
||||||
|
Конфигурация TFTP сервера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root_dir: Корневая директория для файлов
|
||||||
|
host: IP-адрес для прослушивания
|
||||||
|
port: Порт сервера
|
||||||
|
timeout: Таймаут операций
|
||||||
|
"""
|
||||||
|
if root_dir is not None:
|
||||||
|
self._root_dir = root_dir
|
||||||
|
os.makedirs(self._root_dir, exist_ok=True)
|
||||||
|
if host is not None:
|
||||||
|
self._host = host
|
||||||
|
if port is not None:
|
||||||
|
self._port = port
|
||||||
|
if timeout is not None:
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
Запуск TFTP сервера.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TFTPError: При ошибке запуска сервера
|
||||||
|
"""
|
||||||
|
if self.is_running:
|
||||||
|
self._logger.warning("TFTP сервер уже запущен")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем серверный объект
|
||||||
|
self._server = tftpy.TftpServer(self._root_dir)
|
||||||
|
|
||||||
|
# Запускаем сервер в отдельном потоке
|
||||||
|
self._start_in_thread(self._serve)
|
||||||
|
|
||||||
|
self._notify_started({
|
||||||
|
"host": self._host,
|
||||||
|
"port": self._port,
|
||||||
|
"root_dir": self._root_dir
|
||||||
|
})
|
||||||
|
|
||||||
|
self._logger.info(f"TFTP сервер запущен на {self._host}:{self._port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._notify_error(e)
|
||||||
|
raise TFTPError(f"Ошибка запуска TFTP сервера: {e}")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Остановка TFTP сервера."""
|
||||||
|
if not self.is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._server:
|
||||||
|
self._server.stop()
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
self._stop_thread()
|
||||||
|
self._notify_stopped()
|
||||||
|
|
||||||
|
self._logger.info("TFTP сервер остановлен")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка при остановке TFTP сервера: {e}")
|
||||||
|
|
||||||
|
def _serve(self) -> None:
|
||||||
|
"""Основной цикл сервера."""
|
||||||
|
try:
|
||||||
|
self._server.listen(self._host, self._port, timeout=self._timeout)
|
||||||
|
except Exception as e:
|
||||||
|
self._notify_error(e)
|
||||||
|
self._logger.error(f"Ошибка в работе TFTP сервера: {e}")
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def get_server_info(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Получение информации о сервере.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Информация о сервере
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"running": self.is_running,
|
||||||
|
"host": self._host,
|
||||||
|
"port": self._port,
|
||||||
|
"root_dir": self._root_dir,
|
||||||
|
"timeout": self._timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_files(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Получение списка файлов в корневой директории.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: Список файлов
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return os.listdir(self._root_dir)
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка при получении списка файлов: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_file_info(self, filename: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Получение информации о файле.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Dict[str, Any]]: Информация о файле или None
|
||||||
|
"""
|
||||||
|
file_path = os.path.join(self._root_dir, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
stat = os.stat(file_path)
|
||||||
|
return {
|
||||||
|
"name": filename,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"modified": stat.st_mtime,
|
||||||
|
"created": stat.st_ctime
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка при получении информации о файле: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_file(self, filename: str) -> bool:
|
||||||
|
"""
|
||||||
|
Удаление файла из корневой директории.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Имя файла
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если файл удален успешно
|
||||||
|
"""
|
||||||
|
file_path = os.path.join(self._root_dir, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
self._logger.info(f"Файл удален: {filename}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка при удалении файла: {e}")
|
||||||
|
return False
|
||||||
0
src/network/transfer/__init__.py
Normal file
0
src/network/transfer/__init__.py
Normal file
200
src/network/transfer/progress_tracker.py
Normal file
200
src/network/transfer/progress_tracker.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Optional, Dict, Any, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TransferProgress:
|
||||||
|
"""Информация о прогрессе передачи."""
|
||||||
|
total_bytes: int
|
||||||
|
transferred_bytes: int = 0
|
||||||
|
start_time: float = 0.0
|
||||||
|
end_time: float = 0.0
|
||||||
|
status: str = "pending"
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def progress(self) -> float:
|
||||||
|
"""Процент выполнения."""
|
||||||
|
if self.total_bytes == 0:
|
||||||
|
return 0.0
|
||||||
|
return (self.transferred_bytes / self.total_bytes) * 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed(self) -> float:
|
||||||
|
"""Скорость передачи в байтах в секунду."""
|
||||||
|
if self.start_time == 0 or self.transferred_bytes == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
duration = (self.end_time or time.time()) - self.start_time
|
||||||
|
if duration == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return self.transferred_bytes / duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def elapsed_time(self) -> float:
|
||||||
|
"""Прошедшее время в секундах."""
|
||||||
|
if self.start_time == 0:
|
||||||
|
return 0.0
|
||||||
|
return (self.end_time or time.time()) - self.start_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def estimated_time(self) -> float:
|
||||||
|
"""Оценка оставшегося времени в секундах."""
|
||||||
|
if self.speed == 0 or self.transferred_bytes == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
remaining_bytes = self.total_bytes - self.transferred_bytes
|
||||||
|
return remaining_bytes / self.speed
|
||||||
|
|
||||||
|
class ProgressTracker:
|
||||||
|
"""Трекер прогресса передачи файлов."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._transfers: Dict[str, TransferProgress] = {}
|
||||||
|
self._callbacks: Dict[str, Callable[[TransferProgress], None]] = {}
|
||||||
|
|
||||||
|
def start_transfer(self, transfer_id: str, total_bytes: int,
|
||||||
|
callback: Optional[Callable[[TransferProgress], None]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Начало отслеживания передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
total_bytes: Общий размер в байтах
|
||||||
|
callback: Функция обратного вызова
|
||||||
|
"""
|
||||||
|
progress = TransferProgress(
|
||||||
|
total_bytes=total_bytes,
|
||||||
|
start_time=time.time()
|
||||||
|
)
|
||||||
|
|
||||||
|
self._transfers[transfer_id] = progress
|
||||||
|
if callback:
|
||||||
|
self._callbacks[transfer_id] = callback
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TRANSFER_STARTED, {
|
||||||
|
"transfer_id": transfer_id,
|
||||||
|
"total_bytes": total_bytes
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info(f"Начата передача {transfer_id}: {total_bytes} байт")
|
||||||
|
|
||||||
|
def update_progress(self, transfer_id: str, transferred_bytes: int) -> None:
|
||||||
|
"""
|
||||||
|
Обновление прогресса передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
transferred_bytes: Количество переданных байт
|
||||||
|
"""
|
||||||
|
if transfer_id not in self._transfers:
|
||||||
|
self._logger.warning(f"Передача {transfer_id} не найдена")
|
||||||
|
return
|
||||||
|
|
||||||
|
progress = self._transfers[transfer_id]
|
||||||
|
progress.transferred_bytes = transferred_bytes
|
||||||
|
|
||||||
|
# Вызываем callback, если есть
|
||||||
|
if transfer_id in self._callbacks:
|
||||||
|
self._callbacks[transfer_id](progress)
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TRANSFER_PROGRESS, {
|
||||||
|
"transfer_id": transfer_id,
|
||||||
|
"progress": progress.progress,
|
||||||
|
"speed": progress.speed,
|
||||||
|
"elapsed_time": progress.elapsed_time,
|
||||||
|
"estimated_time": progress.estimated_time
|
||||||
|
}))
|
||||||
|
|
||||||
|
def complete_transfer(self, transfer_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Завершение передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
"""
|
||||||
|
if transfer_id not in self._transfers:
|
||||||
|
return
|
||||||
|
|
||||||
|
progress = self._transfers[transfer_id]
|
||||||
|
progress.end_time = time.time()
|
||||||
|
progress.status = "completed"
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TRANSFER_COMPLETED, {
|
||||||
|
"transfer_id": transfer_id,
|
||||||
|
"total_time": progress.elapsed_time,
|
||||||
|
"average_speed": progress.speed
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.info(
|
||||||
|
f"Завершена передача {transfer_id}: "
|
||||||
|
f"{progress.transferred_bytes}/{progress.total_bytes} байт "
|
||||||
|
f"за {progress.elapsed_time:.1f} сек "
|
||||||
|
f"({progress.speed:.1f} байт/сек)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cleanup_transfer(transfer_id)
|
||||||
|
|
||||||
|
def fail_transfer(self, transfer_id: str, error: str) -> None:
|
||||||
|
"""
|
||||||
|
Отметка передачи как неудачной.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
error: Сообщение об ошибке
|
||||||
|
"""
|
||||||
|
if transfer_id not in self._transfers:
|
||||||
|
return
|
||||||
|
|
||||||
|
progress = self._transfers[transfer_id]
|
||||||
|
progress.end_time = time.time()
|
||||||
|
progress.status = "failed"
|
||||||
|
progress.error = error
|
||||||
|
|
||||||
|
event_bus.publish(Event(EventTypes.TRANSFER_ERROR, {
|
||||||
|
"transfer_id": transfer_id,
|
||||||
|
"error": error
|
||||||
|
}))
|
||||||
|
|
||||||
|
self._logger.error(f"Ошибка передачи {transfer_id}: {error}")
|
||||||
|
|
||||||
|
self._cleanup_transfer(transfer_id)
|
||||||
|
|
||||||
|
def get_progress(self, transfer_id: str) -> Optional[TransferProgress]:
|
||||||
|
"""
|
||||||
|
Получение информации о прогрессе передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[TransferProgress]: Информация о прогрессе или None
|
||||||
|
"""
|
||||||
|
return self._transfers.get(transfer_id)
|
||||||
|
|
||||||
|
def list_transfers(self) -> Dict[str, TransferProgress]:
|
||||||
|
"""
|
||||||
|
Получение списка всех передач.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, TransferProgress]: Словарь с информацией о передачах
|
||||||
|
"""
|
||||||
|
return self._transfers.copy()
|
||||||
|
|
||||||
|
def _cleanup_transfer(self, transfer_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Очистка данных завершенной передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
"""
|
||||||
|
self._transfers.pop(transfer_id, None)
|
||||||
|
self._callbacks.pop(transfer_id, None)
|
||||||
335
src/network/transfer/transfer_manager.py
Normal file
335
src/network/transfer/transfer_manager.py
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from typing import Optional, Dict, Any, List, Callable
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.exceptions import TransferError, ValidationError
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
from communication.serial_manager import SerialManager
|
||||||
|
from network.servers.tftp_server import TFTPServer
|
||||||
|
from .progress_tracker import ProgressTracker
|
||||||
|
|
||||||
|
class TransferManager:
|
||||||
|
"""Менеджер передачи файлов."""
|
||||||
|
|
||||||
|
def __init__(self, serial_manager: SerialManager, tftp_server: TFTPServer):
|
||||||
|
self._serial = serial_manager
|
||||||
|
self._tftp = tftp_server
|
||||||
|
self._progress = ProgressTracker()
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._active_transfers: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def transfer_config(self, config_path: str, mode: str = "line",
|
||||||
|
block_size: int = AppConfig.DEFAULT_BLOCK_SIZE,
|
||||||
|
callback: Optional[Callable[[float], None]] = None) -> str:
|
||||||
|
"""
|
||||||
|
Передача конфигурации на устройство.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Путь к файлу конфигурации
|
||||||
|
mode: Режим передачи (line/block/tftp)
|
||||||
|
block_size: Размер блока команд
|
||||||
|
callback: Функция обратного вызова для отслеживания прогресса
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Идентификатор передачи
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: При ошибке валидации параметров
|
||||||
|
TransferError: При ошибке передачи
|
||||||
|
"""
|
||||||
|
# Проверяем файл
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
raise ValidationError(f"Файл не найден: {config_path}")
|
||||||
|
|
||||||
|
# Проверяем режим передачи
|
||||||
|
if mode not in AppConfig.SUPPORTED_COPY_MODES:
|
||||||
|
raise ValidationError(f"Неподдерживаемый режим передачи: {mode}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем идентификатор передачи
|
||||||
|
transfer_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Читаем конфигурацию
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
|
config_data = f.read()
|
||||||
|
|
||||||
|
# Разбиваем на команды
|
||||||
|
commands = [cmd.strip() for cmd in config_data.splitlines() if cmd.strip()]
|
||||||
|
total_commands = len(commands)
|
||||||
|
|
||||||
|
if not commands:
|
||||||
|
raise ValidationError("Пустой файл конфигурации")
|
||||||
|
|
||||||
|
# Сохраняем информацию о передаче
|
||||||
|
self._active_transfers[transfer_id] = {
|
||||||
|
"mode": mode,
|
||||||
|
"file": config_path,
|
||||||
|
"total_commands": total_commands,
|
||||||
|
"current_command": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Запускаем передачу в зависимости от режима
|
||||||
|
if mode == "line":
|
||||||
|
self._transfer_line_by_line(transfer_id, commands, callback)
|
||||||
|
elif mode == "block":
|
||||||
|
self._transfer_by_blocks(transfer_id, commands, block_size, callback)
|
||||||
|
elif mode == "tftp":
|
||||||
|
self._transfer_by_tftp(transfer_id, config_path, callback)
|
||||||
|
|
||||||
|
return transfer_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка передачи конфигурации: {e}")
|
||||||
|
raise TransferError(f"Ошибка передачи конфигурации: {e}")
|
||||||
|
|
||||||
|
def _transfer_line_by_line(self, transfer_id: str, commands: List[str],
|
||||||
|
callback: Optional[Callable[[float], None]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Построчная передача команд.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
commands: Список команд
|
||||||
|
callback: Функция обратного вызова
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TransferError: При ошибке передачи
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
total_commands = len(commands)
|
||||||
|
|
||||||
|
# Начинаем отслеживание прогресса
|
||||||
|
self._progress.start_transfer(transfer_id, total_commands, callback)
|
||||||
|
|
||||||
|
# Отправляем команды по одной
|
||||||
|
for i, command in enumerate(commands, 1):
|
||||||
|
response = self._serial.send_command(command)
|
||||||
|
|
||||||
|
# Проверяем ответ на ошибки
|
||||||
|
if "error" in response.lower() or "invalid" in response.lower():
|
||||||
|
raise TransferError(f"Ошибка выполнения команды: {response}")
|
||||||
|
|
||||||
|
# Обновляем прогресс
|
||||||
|
self._progress.update_progress(transfer_id, i)
|
||||||
|
|
||||||
|
# Обновляем информацию о передаче
|
||||||
|
self._active_transfers[transfer_id]["current_command"] = i
|
||||||
|
|
||||||
|
# Завершаем передачу
|
||||||
|
self._progress.complete_transfer(transfer_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._progress.fail_transfer(transfer_id, str(e))
|
||||||
|
raise TransferError(f"Ошибка построчной передачи: {e}")
|
||||||
|
|
||||||
|
def _transfer_by_blocks(self, transfer_id: str, commands: List[str],
|
||||||
|
block_size: int,
|
||||||
|
callback: Optional[Callable[[float], None]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Блочная передача команд.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
commands: Список команд
|
||||||
|
block_size: Размер блока
|
||||||
|
callback: Функция обратного вызова
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TransferError: При ошибке передачи
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
total_commands = len(commands)
|
||||||
|
|
||||||
|
# Начинаем отслеживание прогресса
|
||||||
|
self._progress.start_transfer(transfer_id, total_commands, callback)
|
||||||
|
|
||||||
|
# Разбиваем команды на блоки
|
||||||
|
for i in range(0, total_commands, block_size):
|
||||||
|
block = commands[i:i + block_size]
|
||||||
|
|
||||||
|
# Отправляем блок команд
|
||||||
|
response = self._serial.send_config(block)
|
||||||
|
|
||||||
|
# Проверяем ответ на ошибки
|
||||||
|
if "error" in response.lower() or "invalid" in response.lower():
|
||||||
|
raise TransferError(f"Ошибка выполнения блока команд: {response}")
|
||||||
|
|
||||||
|
# Обновляем прогресс
|
||||||
|
self._progress.update_progress(transfer_id, i + len(block))
|
||||||
|
|
||||||
|
# Обновляем информацию о передаче
|
||||||
|
self._active_transfers[transfer_id]["current_command"] = i + len(block)
|
||||||
|
|
||||||
|
# Завершаем передачу
|
||||||
|
self._progress.complete_transfer(transfer_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._progress.fail_transfer(transfer_id, str(e))
|
||||||
|
raise TransferError(f"Ошибка блочной передачи: {e}")
|
||||||
|
|
||||||
|
def _transfer_by_tftp(self, transfer_id: str, config_path: str,
|
||||||
|
callback: Optional[Callable[[float], None]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Передача через TFTP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
config_path: Путь к файлу конфигурации
|
||||||
|
callback: Функция обратного вызова
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TransferError: При ошибке передачи
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Получаем размер файла
|
||||||
|
file_size = os.path.getsize(config_path)
|
||||||
|
|
||||||
|
# Начинаем отслеживание прогресса
|
||||||
|
self._progress.start_transfer(transfer_id, file_size, callback)
|
||||||
|
|
||||||
|
# Запускаем TFTP сервер, если не запущен
|
||||||
|
if not self._tftp.is_running:
|
||||||
|
self._tftp.start()
|
||||||
|
|
||||||
|
# Копируем файл в директорию TFTP сервера
|
||||||
|
filename = os.path.basename(config_path)
|
||||||
|
tftp_path = os.path.join(self._tftp.root_dir, filename)
|
||||||
|
|
||||||
|
with open(config_path, "rb") as src, open(tftp_path, "wb") as dst:
|
||||||
|
dst.write(src.read())
|
||||||
|
|
||||||
|
# Отправляем команду на загрузку файла через TFTP
|
||||||
|
command = f"copy tftp://{self._tftp.host}/{filename} startup-config"
|
||||||
|
response = self._serial.send_command(command)
|
||||||
|
|
||||||
|
# Проверяем ответ на ошибки
|
||||||
|
if "error" in response.lower() or "invalid" in response.lower():
|
||||||
|
raise TransferError(f"Ошибка загрузки через TFTP: {response}")
|
||||||
|
|
||||||
|
# Завершаем передачу
|
||||||
|
self._progress.complete_transfer(transfer_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._progress.fail_transfer(transfer_id, str(e))
|
||||||
|
raise TransferError(f"Ошибка передачи через TFTP: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Удаляем временный файл
|
||||||
|
if os.path.exists(tftp_path):
|
||||||
|
os.remove(tftp_path)
|
||||||
|
|
||||||
|
def get_transfer_info(self, transfer_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Получение информации о передаче.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Dict[str, Any]]: Информация о передаче или None
|
||||||
|
"""
|
||||||
|
if transfer_id not in self._active_transfers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transfer = self._active_transfers[transfer_id]
|
||||||
|
progress = self._progress.get_progress(transfer_id)
|
||||||
|
|
||||||
|
if not progress:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
**transfer,
|
||||||
|
"progress": progress.progress,
|
||||||
|
"speed": progress.speed,
|
||||||
|
"elapsed_time": progress.elapsed_time,
|
||||||
|
"estimated_time": progress.estimated_time,
|
||||||
|
"status": progress.status,
|
||||||
|
"error": progress.error
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_transfers(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Получение списка активных передач.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: Список с информацией о передачах
|
||||||
|
"""
|
||||||
|
transfers = []
|
||||||
|
|
||||||
|
for transfer_id, transfer in self._active_transfers.items():
|
||||||
|
info = self.get_transfer_info(transfer_id)
|
||||||
|
if info:
|
||||||
|
transfers.append(info)
|
||||||
|
|
||||||
|
return transfers
|
||||||
|
|
||||||
|
def cancel_transfer(self, transfer_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Отмена передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
"""
|
||||||
|
if transfer_id in self._active_transfers:
|
||||||
|
self._progress.fail_transfer(transfer_id, "Передача отменена пользователем")
|
||||||
|
self._active_transfers.pop(transfer_id)
|
||||||
|
|
||||||
|
def is_transfer_active(self, transfer_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверка активности передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transfer_id: Идентификатор передачи
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если передача активна
|
||||||
|
"""
|
||||||
|
if transfer_id not in self._active_transfers:
|
||||||
|
return False
|
||||||
|
|
||||||
|
progress = self._progress.get_progress(transfer_id)
|
||||||
|
if not progress:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return progress.status not in ["completed", "failed"]
|
||||||
|
|
||||||
|
def cleanup_transfers(self) -> None:
|
||||||
|
"""Очистка завершенных передач."""
|
||||||
|
completed_transfers = [
|
||||||
|
transfer_id for transfer_id in self._active_transfers
|
||||||
|
if not self.is_transfer_active(transfer_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
for transfer_id in completed_transfers:
|
||||||
|
self._active_transfers.pop(transfer_id)
|
||||||
|
|
||||||
|
self._logger.debug(f"Очищено {len(completed_transfers)} завершенных передач")
|
||||||
|
|
||||||
|
def get_active_transfer_count(self) -> int:
|
||||||
|
"""
|
||||||
|
Получение количества активных передач.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Количество активных передач
|
||||||
|
"""
|
||||||
|
return len([
|
||||||
|
transfer_id for transfer_id in self._active_transfers
|
||||||
|
if self.is_transfer_active(transfer_id)
|
||||||
|
])
|
||||||
|
|
||||||
|
def cancel_all_transfers(self) -> None:
|
||||||
|
"""Отмена всех активных передач."""
|
||||||
|
active_transfers = [
|
||||||
|
transfer_id for transfer_id in self._active_transfers
|
||||||
|
if self.is_transfer_active(transfer_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
for transfer_id in active_transfers:
|
||||||
|
self.cancel_transfer(transfer_id)
|
||||||
|
|
||||||
|
self._logger.info(f"Отменено {len(active_transfers)} активных передач")
|
||||||
0
src/network/utils/__init__.py
Normal file
0
src/network/utils/__init__.py
Normal file
0
src/network/utils/network_scanner.py
Normal file
0
src/network/utils/network_scanner.py
Normal file
0
src/network/utils/port_checker.py
Normal file
0
src/network/utils/port_checker.py
Normal file
4
src/requirements.txt
Normal file
4
src/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pyserial>=3.5
|
||||||
|
tftpy>=0.8.0
|
||||||
|
requests>=2.31.0
|
||||||
|
watchdog>=3.0.0
|
||||||
0
src/ui/__init__.py
Normal file
0
src/ui/__init__.py
Normal file
0
src/ui/dialogs/__init__.py
Normal file
0
src/ui/dialogs/__init__.py
Normal file
250
src/ui/dialogs/about_dialog.py
Normal file
250
src/ui/dialogs/about_dialog.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
import webbrowser
|
||||||
|
from typing import Optional
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
|
||||||
|
class AboutDialog(tk.Toplevel):
|
||||||
|
"""Диалог с информацией о программе."""
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.title("О программе")
|
||||||
|
self.resizable(False, False)
|
||||||
|
self.transient(parent)
|
||||||
|
|
||||||
|
# Создаем элементы интерфейса
|
||||||
|
self._create_widgets()
|
||||||
|
|
||||||
|
# Делаем диалог модальным
|
||||||
|
self.grab_set()
|
||||||
|
self.focus_set()
|
||||||
|
|
||||||
|
# Центрируем окно
|
||||||
|
self._center_window()
|
||||||
|
|
||||||
|
def _create_widgets(self) -> None:
|
||||||
|
"""Создание элементов интерфейса."""
|
||||||
|
# Основной контейнер
|
||||||
|
main_frame = ttk.Frame(self, padding="20")
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Логотип
|
||||||
|
try:
|
||||||
|
logo = tk.PhotoImage(file="Icon.ico")
|
||||||
|
logo_label = ttk.Label(main_frame, image=logo)
|
||||||
|
logo_label.image = logo # Сохраняем ссылку
|
||||||
|
logo_label.pack(pady=(0, 10))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Название программы
|
||||||
|
ttk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="ComConfigCopy",
|
||||||
|
font=("Arial", 14, "bold")
|
||||||
|
).pack()
|
||||||
|
|
||||||
|
# Версия
|
||||||
|
ttk.Label(
|
||||||
|
main_frame,
|
||||||
|
text=f"Версия {AppConfig.VERSION}",
|
||||||
|
font=("Arial", 10)
|
||||||
|
).pack()
|
||||||
|
|
||||||
|
# Описание
|
||||||
|
description = (
|
||||||
|
"Программа для копирования конфигураций\n"
|
||||||
|
"на сетевое оборудование через последовательный порт и TFTP"
|
||||||
|
)
|
||||||
|
ttk.Label(
|
||||||
|
main_frame,
|
||||||
|
text=description,
|
||||||
|
justify=tk.CENTER,
|
||||||
|
wraplength=300
|
||||||
|
).pack(pady=10)
|
||||||
|
|
||||||
|
# Системная информация
|
||||||
|
system_frame = ttk.LabelFrame(main_frame, text="Системная информация", padding="5")
|
||||||
|
system_frame.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
# Python
|
||||||
|
ttk.Label(
|
||||||
|
system_frame,
|
||||||
|
text=f"Python: {sys.version.split()[0]}"
|
||||||
|
).pack(anchor=tk.W)
|
||||||
|
|
||||||
|
# ОС
|
||||||
|
ttk.Label(
|
||||||
|
system_frame,
|
||||||
|
text=f"ОС: {platform.system()} {platform.release()}"
|
||||||
|
).pack(anchor=tk.W)
|
||||||
|
|
||||||
|
# Процессор
|
||||||
|
ttk.Label(
|
||||||
|
system_frame,
|
||||||
|
text=f"Процессор: {platform.processor()}"
|
||||||
|
).pack(anchor=tk.W)
|
||||||
|
|
||||||
|
# Зависимости
|
||||||
|
deps_frame = ttk.LabelFrame(main_frame, text="Зависимости", padding="5")
|
||||||
|
deps_frame.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
# Создаем список зависимостей
|
||||||
|
self._add_dependency(deps_frame, "pyserial", ">=3.5")
|
||||||
|
self._add_dependency(deps_frame, "tftpy", ">=0.8.0")
|
||||||
|
self._add_dependency(deps_frame, "requests", ">=2.31.0")
|
||||||
|
self._add_dependency(deps_frame, "watchdog", ">=3.0.0")
|
||||||
|
|
||||||
|
# Ссылки
|
||||||
|
links_frame = ttk.Frame(main_frame)
|
||||||
|
links_frame.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
# GitHub
|
||||||
|
github_link = ttk.Label(
|
||||||
|
links_frame,
|
||||||
|
text="GitHub",
|
||||||
|
cursor="hand2",
|
||||||
|
foreground="blue"
|
||||||
|
)
|
||||||
|
github_link.pack(side=tk.LEFT, padx=5)
|
||||||
|
github_link.bind(
|
||||||
|
"<Button-1>",
|
||||||
|
lambda e: webbrowser.open("https://github.com/yourusername/ComConfigCopy")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Документация
|
||||||
|
docs_link = ttk.Label(
|
||||||
|
links_frame,
|
||||||
|
text="Документация",
|
||||||
|
cursor="hand2",
|
||||||
|
foreground="blue"
|
||||||
|
)
|
||||||
|
docs_link.pack(side=tk.LEFT, padx=5)
|
||||||
|
docs_link.bind(
|
||||||
|
"<Button-1>",
|
||||||
|
lambda e: webbrowser.open("https://yourusername.github.io/ComConfigCopy")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Лицензия
|
||||||
|
license_link = ttk.Label(
|
||||||
|
links_frame,
|
||||||
|
text="Лицензия",
|
||||||
|
cursor="hand2",
|
||||||
|
foreground="blue"
|
||||||
|
)
|
||||||
|
license_link.pack(side=tk.LEFT, padx=5)
|
||||||
|
license_link.bind(
|
||||||
|
"<Button-1>",
|
||||||
|
lambda e: self._show_license()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Кнопка закрытия
|
||||||
|
ttk.Button(
|
||||||
|
main_frame,
|
||||||
|
text="Закрыть",
|
||||||
|
command=self.destroy
|
||||||
|
).pack(pady=(10, 0))
|
||||||
|
|
||||||
|
def _add_dependency(self, parent: ttk.Frame, name: str, version: str) -> None:
|
||||||
|
"""
|
||||||
|
Добавление зависимости в список.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Родительский виджет
|
||||||
|
name: Название пакета
|
||||||
|
version: Версия пакета
|
||||||
|
"""
|
||||||
|
frame = ttk.Frame(parent)
|
||||||
|
frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
ttk.Label(frame, text=name).pack(side=tk.LEFT)
|
||||||
|
ttk.Label(frame, text=version).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _show_license(self) -> None:
|
||||||
|
"""Отображение текста лицензии."""
|
||||||
|
license_text = """
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Your Name
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
"""
|
||||||
|
# Создаем диалог с текстом лицензии
|
||||||
|
dialog = tk.Toplevel(self)
|
||||||
|
dialog.title("Лицензия")
|
||||||
|
dialog.transient(self)
|
||||||
|
dialog.grab_set()
|
||||||
|
|
||||||
|
# Добавляем текстовое поле
|
||||||
|
text = tk.Text(
|
||||||
|
dialog,
|
||||||
|
wrap=tk.WORD,
|
||||||
|
width=60,
|
||||||
|
height=20,
|
||||||
|
padx=10,
|
||||||
|
pady=10
|
||||||
|
)
|
||||||
|
text.pack(padx=10, pady=10)
|
||||||
|
|
||||||
|
# Вставляем текст лицензии
|
||||||
|
text.insert("1.0", license_text.strip())
|
||||||
|
text.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Добавляем кнопку закрытия
|
||||||
|
ttk.Button(
|
||||||
|
dialog,
|
||||||
|
text="Закрыть",
|
||||||
|
command=dialog.destroy
|
||||||
|
).pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# Центрируем диалог
|
||||||
|
self._center_window(dialog)
|
||||||
|
|
||||||
|
def _center_window(self, window: Optional[tk.Toplevel] = None) -> None:
|
||||||
|
"""
|
||||||
|
Центрирование окна на экране.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window: Окно для центрирования
|
||||||
|
"""
|
||||||
|
window = window or self
|
||||||
|
window.update_idletasks()
|
||||||
|
|
||||||
|
# Получаем размеры экрана
|
||||||
|
screen_width = window.winfo_screenwidth()
|
||||||
|
screen_height = window.winfo_screenheight()
|
||||||
|
|
||||||
|
# Получаем размеры окна
|
||||||
|
window_width = window.winfo_width()
|
||||||
|
window_height = window.winfo_height()
|
||||||
|
|
||||||
|
# Вычисляем координаты
|
||||||
|
x = (screen_width - window_width) // 2
|
||||||
|
y = (screen_height - window_height) // 2
|
||||||
|
|
||||||
|
# Устанавливаем позицию
|
||||||
|
window.geometry(f"+{x}+{y}")
|
||||||
227
src/ui/dialogs/settings_dialog.py
Normal file
227
src/ui/dialogs/settings_dialog.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
from ..widgets.custom_entry import CustomEntry
|
||||||
|
|
||||||
|
class SettingsDialog(tk.Toplevel):
|
||||||
|
"""Диалог настроек приложения."""
|
||||||
|
|
||||||
|
def __init__(self, parent, settings: Dict[str, Any]):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.title("Настройки")
|
||||||
|
self.resizable(False, False)
|
||||||
|
self.transient(parent)
|
||||||
|
|
||||||
|
# Сохраняем текущие настройки
|
||||||
|
self._settings = settings.copy()
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Создаем элементы интерфейса
|
||||||
|
self._create_widgets()
|
||||||
|
|
||||||
|
# Заполняем поля текущими значениями
|
||||||
|
self._load_settings()
|
||||||
|
|
||||||
|
# Делаем диалог модальным
|
||||||
|
self.grab_set()
|
||||||
|
self.focus_set()
|
||||||
|
|
||||||
|
def _create_widgets(self) -> None:
|
||||||
|
"""Создание элементов интерфейса."""
|
||||||
|
# Основной контейнер
|
||||||
|
main_frame = ttk.Frame(self, padding="10")
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Настройки последовательного порта
|
||||||
|
port_frame = ttk.LabelFrame(main_frame, text="Последовательный порт", padding="5")
|
||||||
|
port_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# COM-порт
|
||||||
|
ttk.Label(port_frame, text="COM-порт:").grid(row=0, column=0, sticky=tk.W)
|
||||||
|
self.port_entry = CustomEntry(port_frame)
|
||||||
|
self.port_entry.grid(row=0, column=1, sticky=tk.EW, padx=5)
|
||||||
|
|
||||||
|
# Скорость
|
||||||
|
ttk.Label(port_frame, text="Скорость:").grid(row=1, column=0, sticky=tk.W)
|
||||||
|
self.baudrate_combo = ttk.Combobox(
|
||||||
|
port_frame,
|
||||||
|
values=AppConfig.SUPPORTED_BAUDRATES,
|
||||||
|
state="readonly"
|
||||||
|
)
|
||||||
|
self.baudrate_combo.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Таймаут
|
||||||
|
ttk.Label(port_frame, text="Таймаут (сек):").grid(row=2, column=0, sticky=tk.W)
|
||||||
|
self.timeout_entry = CustomEntry(
|
||||||
|
port_frame,
|
||||||
|
validator=lambda x: x.isdigit() and 1 <= int(x) <= 60
|
||||||
|
)
|
||||||
|
self.timeout_entry.grid(row=2, column=1, sticky=tk.EW, padx=5)
|
||||||
|
|
||||||
|
# Настройки передачи
|
||||||
|
transfer_frame = ttk.LabelFrame(main_frame, text="Передача данных", padding="5")
|
||||||
|
transfer_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Режим копирования
|
||||||
|
ttk.Label(transfer_frame, text="Режим:").grid(row=0, column=0, sticky=tk.W)
|
||||||
|
self.copy_mode_combo = ttk.Combobox(
|
||||||
|
transfer_frame,
|
||||||
|
values=AppConfig.SUPPORTED_COPY_MODES,
|
||||||
|
state="readonly"
|
||||||
|
)
|
||||||
|
self.copy_mode_combo.grid(row=0, column=1, sticky=tk.EW, padx=5)
|
||||||
|
|
||||||
|
# Размер блока
|
||||||
|
ttk.Label(transfer_frame, text="Размер блока:").grid(row=1, column=0, sticky=tk.W)
|
||||||
|
self.block_size_entry = CustomEntry(
|
||||||
|
transfer_frame,
|
||||||
|
validator=lambda x: x.isdigit() and 1 <= int(x) <= 100
|
||||||
|
)
|
||||||
|
self.block_size_entry.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Приглашение командной строки
|
||||||
|
ttk.Label(transfer_frame, text="Приглашение:").grid(row=2, column=0, sticky=tk.W)
|
||||||
|
self.prompt_entry = CustomEntry(transfer_frame)
|
||||||
|
self.prompt_entry.grid(row=2, column=1, sticky=tk.EW, padx=5)
|
||||||
|
|
||||||
|
# Настройки TFTP
|
||||||
|
tftp_frame = ttk.LabelFrame(main_frame, text="TFTP сервер", padding="5")
|
||||||
|
tftp_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Порт TFTP
|
||||||
|
ttk.Label(tftp_frame, text="Порт:").grid(row=0, column=0, sticky=tk.W)
|
||||||
|
self.tftp_port_entry = CustomEntry(
|
||||||
|
tftp_frame,
|
||||||
|
validator=lambda x: x.isdigit() and 1 <= int(x) <= 65535
|
||||||
|
)
|
||||||
|
self.tftp_port_entry.grid(row=0, column=1, sticky=tk.EW, padx=5)
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
button_frame = ttk.Frame(main_frame)
|
||||||
|
button_frame.pack(fill=tk.X, padx=5, pady=10)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Сохранить",
|
||||||
|
command=self._save_settings
|
||||||
|
).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Отмена",
|
||||||
|
command=self.destroy
|
||||||
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# Настраиваем растяжение колонок
|
||||||
|
port_frame.columnconfigure(1, weight=1)
|
||||||
|
transfer_frame.columnconfigure(1, weight=1)
|
||||||
|
tftp_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
def _load_settings(self) -> None:
|
||||||
|
"""Загрузка текущих настроек в поля ввода."""
|
||||||
|
# COM-порт
|
||||||
|
self.port_entry.set(self._settings.get("port", ""))
|
||||||
|
|
||||||
|
# Скорость
|
||||||
|
baudrate = str(self._settings.get("baudrate", AppConfig.DEFAULT_BAUDRATE))
|
||||||
|
self.baudrate_combo.set(baudrate)
|
||||||
|
|
||||||
|
# Таймаут
|
||||||
|
timeout = str(self._settings.get("timeout", AppConfig.DEFAULT_TIMEOUT))
|
||||||
|
self.timeout_entry.set(timeout)
|
||||||
|
|
||||||
|
# Режим копирования
|
||||||
|
copy_mode = self._settings.get("copy_mode", AppConfig.DEFAULT_COPY_MODE)
|
||||||
|
self.copy_mode_combo.set(copy_mode)
|
||||||
|
|
||||||
|
# Размер блока
|
||||||
|
block_size = str(self._settings.get("block_size", AppConfig.DEFAULT_BLOCK_SIZE))
|
||||||
|
self.block_size_entry.set(block_size)
|
||||||
|
|
||||||
|
# Приглашение
|
||||||
|
prompt = self._settings.get("prompt", AppConfig.DEFAULT_PROMPT)
|
||||||
|
self.prompt_entry.set(prompt)
|
||||||
|
|
||||||
|
# Порт TFTP
|
||||||
|
tftp_port = str(self._settings.get("tftp_port", AppConfig.TFTP_PORT))
|
||||||
|
self.tftp_port_entry.set(tftp_port)
|
||||||
|
|
||||||
|
def _validate_settings(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Валидация введенных настроек.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Сообщение об ошибке или None
|
||||||
|
"""
|
||||||
|
# Проверяем COM-порт
|
||||||
|
if not self.port_entry.get():
|
||||||
|
return "Не указан COM-порт"
|
||||||
|
|
||||||
|
# Проверяем таймаут
|
||||||
|
try:
|
||||||
|
timeout = int(self.timeout_entry.get())
|
||||||
|
if not 1 <= timeout <= 60:
|
||||||
|
return "Таймаут должен быть от 1 до 60 секунд"
|
||||||
|
except ValueError:
|
||||||
|
return "Некорректное значение таймаута"
|
||||||
|
|
||||||
|
# Проверяем размер блока
|
||||||
|
try:
|
||||||
|
block_size = int(self.block_size_entry.get())
|
||||||
|
if not 1 <= block_size <= 100:
|
||||||
|
return "Размер блока должен быть от 1 до 100"
|
||||||
|
except ValueError:
|
||||||
|
return "Некорректное значение размера блока"
|
||||||
|
|
||||||
|
# Проверяем порт TFTP
|
||||||
|
try:
|
||||||
|
tftp_port = int(self.tftp_port_entry.get())
|
||||||
|
if not 1 <= tftp_port <= 65535:
|
||||||
|
return "Порт TFTP должен быть от 1 до 65535"
|
||||||
|
except ValueError:
|
||||||
|
return "Некорректное значение порта TFTP"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_settings(self) -> None:
|
||||||
|
"""Сохранение настроек."""
|
||||||
|
# Проверяем настройки
|
||||||
|
error = self._validate_settings()
|
||||||
|
if error:
|
||||||
|
tk.messagebox.showerror("Ошибка", error)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Обновляем настройки
|
||||||
|
self._settings.update({
|
||||||
|
"port": self.port_entry.get(),
|
||||||
|
"baudrate": int(self.baudrate_combo.get()),
|
||||||
|
"timeout": int(self.timeout_entry.get()),
|
||||||
|
"copy_mode": self.copy_mode_combo.get(),
|
||||||
|
"block_size": int(self.block_size_entry.get()),
|
||||||
|
"prompt": self.prompt_entry.get(),
|
||||||
|
"tftp_port": int(self.tftp_port_entry.get())
|
||||||
|
})
|
||||||
|
|
||||||
|
# Уведомляем об изменении настроек
|
||||||
|
event_bus.publish(Event(EventTypes.UI_SETTINGS_CHANGED, self._settings))
|
||||||
|
|
||||||
|
self._logger.info("Настройки сохранены")
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка сохранения настроек: {e}")
|
||||||
|
tk.messagebox.showerror("Ошибка", "Не удалось сохранить настройки")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings(self) -> Dict[str, Any]:
|
||||||
|
"""Получение текущих настроек."""
|
||||||
|
return self._settings
|
||||||
741
src/ui/main_window.py
Normal file
741
src/ui/main_window.py
Normal file
@@ -0,0 +1,741 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем директорию src в путь поиска модулей
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import logging
|
||||||
|
import serial.tools.list_ports
|
||||||
|
import os
|
||||||
|
|
||||||
|
from core.config import AppConfig
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
from core.state import ConnectionState, TransferState, StateManager
|
||||||
|
from communication.serial_manager import SerialManager
|
||||||
|
from network.servers.tftp_server import TFTPServer
|
||||||
|
from network.transfer.transfer_manager import TransferManager
|
||||||
|
from filesystem.config_manager import ConfigManager
|
||||||
|
from filesystem.watchers.config_watcher import ConfigWatcher
|
||||||
|
from .widgets.custom_text import CustomText
|
||||||
|
from .widgets.custom_entry import CustomEntry
|
||||||
|
from .widgets.transfer_view import TransferView
|
||||||
|
from .dialogs.settings_dialog import SettingsDialog
|
||||||
|
from .dialogs.about_dialog import AboutDialog
|
||||||
|
|
||||||
|
class MainWindow(tk.Tk):
|
||||||
|
"""Главное окно приложения."""
|
||||||
|
|
||||||
|
def __init__(self, settings, state, serial, transfer, config_manager):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# Настройка окна
|
||||||
|
self.title(f"ComConfigCopy v{AppConfig.VERSION}")
|
||||||
|
self.geometry("800x600")
|
||||||
|
self.minsize(800, 600)
|
||||||
|
|
||||||
|
# Флаг несохраненных изменений
|
||||||
|
self._has_unsaved_changes = False
|
||||||
|
|
||||||
|
# Обработка закрытия окна
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
|
||||||
|
# Инициализация компонентов
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._settings = settings
|
||||||
|
self._state_manager = state
|
||||||
|
self._serial_manager = serial
|
||||||
|
self._transfer_manager = transfer
|
||||||
|
self._config_manager = config_manager
|
||||||
|
self._tftp_server = TFTPServer()
|
||||||
|
self._config_watcher = ConfigWatcher()
|
||||||
|
|
||||||
|
# Создаем интерфейс
|
||||||
|
self._create_widgets()
|
||||||
|
self._setup_event_handlers()
|
||||||
|
|
||||||
|
# Загружаем конфигурации
|
||||||
|
self._load_configs()
|
||||||
|
|
||||||
|
# Запускаем наблюдатель за конфигурациями
|
||||||
|
self._config_watcher.start()
|
||||||
|
self._config_watcher.watch_all_configs()
|
||||||
|
|
||||||
|
def _create_widgets(self) -> None:
|
||||||
|
"""Создание элементов интерфейса."""
|
||||||
|
# Создаем главное меню
|
||||||
|
self._create_menu()
|
||||||
|
|
||||||
|
# Создаем панель инструментов
|
||||||
|
self._create_toolbar()
|
||||||
|
|
||||||
|
# Создаем основной контейнер
|
||||||
|
main_frame = ttk.Frame(self)
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Создаем разделитель для левой и правой панели
|
||||||
|
paned = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
|
||||||
|
paned.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Левая панель
|
||||||
|
left_frame = ttk.Frame(paned)
|
||||||
|
paned.add(left_frame, weight=1)
|
||||||
|
|
||||||
|
# Список конфигураций
|
||||||
|
configs_frame = ttk.LabelFrame(left_frame, text="Конфигурации")
|
||||||
|
configs_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Создаем список с прокруткой
|
||||||
|
self._configs_list = tk.Listbox(
|
||||||
|
configs_frame,
|
||||||
|
selectmode=tk.SINGLE,
|
||||||
|
activestyle=tk.NONE
|
||||||
|
)
|
||||||
|
self._configs_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(configs_frame, orient=tk.VERTICAL)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
self._configs_list.configure(yscrollcommand=scrollbar.set)
|
||||||
|
scrollbar.configure(command=self._configs_list.yview)
|
||||||
|
|
||||||
|
# Кнопки управления конфигурациями
|
||||||
|
config_buttons = ttk.Frame(left_frame)
|
||||||
|
config_buttons.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
config_buttons,
|
||||||
|
text="Добавить",
|
||||||
|
command=self._add_config
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
config_buttons,
|
||||||
|
text="Удалить",
|
||||||
|
command=self._remove_config
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
config_buttons,
|
||||||
|
text="Обновить",
|
||||||
|
command=self._load_configs
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Правая панель
|
||||||
|
right_frame = ttk.Frame(paned)
|
||||||
|
paned.add(right_frame, weight=2)
|
||||||
|
|
||||||
|
# Информация об устройстве
|
||||||
|
device_frame = ttk.LabelFrame(right_frame, text="Информация об устройстве")
|
||||||
|
device_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Создаем сетку для информации
|
||||||
|
self._device_info = {}
|
||||||
|
|
||||||
|
info_grid = ttk.Frame(device_frame)
|
||||||
|
info_grid.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Hostname
|
||||||
|
ttk.Label(info_grid, text="Hostname:").grid(row=0, column=0, sticky=tk.W)
|
||||||
|
self._device_info["hostname"] = ttk.Label(info_grid, text="-")
|
||||||
|
self._device_info["hostname"].grid(row=0, column=1, sticky=tk.W, padx=5)
|
||||||
|
|
||||||
|
# Модель
|
||||||
|
ttk.Label(info_grid, text="Модель:").grid(row=1, column=0, sticky=tk.W)
|
||||||
|
self._device_info["model"] = ttk.Label(info_grid, text="-")
|
||||||
|
self._device_info["model"].grid(row=1, column=1, sticky=tk.W, padx=5)
|
||||||
|
|
||||||
|
# Версия
|
||||||
|
ttk.Label(info_grid, text="Версия:").grid(row=2, column=0, sticky=tk.W)
|
||||||
|
self._device_info["version"] = ttk.Label(info_grid, text="-")
|
||||||
|
self._device_info["version"].grid(row=2, column=1, sticky=tk.W, padx=5)
|
||||||
|
|
||||||
|
# Редактор конфигурации
|
||||||
|
editor_frame = ttk.LabelFrame(right_frame, text="Конфигурация")
|
||||||
|
editor_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
self._editor = CustomText(editor_frame)
|
||||||
|
self._editor.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Виджет прогресса передачи
|
||||||
|
self._transfer_view = TransferView(right_frame)
|
||||||
|
self._transfer_view.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Статусная строка
|
||||||
|
self._status_bar = ttk.Label(
|
||||||
|
self,
|
||||||
|
text="Готов к работе",
|
||||||
|
anchor=tk.W,
|
||||||
|
padding=5
|
||||||
|
)
|
||||||
|
self._status_bar.pack(fill=tk.X)
|
||||||
|
|
||||||
|
def _create_menu(self) -> None:
|
||||||
|
"""Создание главного меню."""
|
||||||
|
menubar = tk.Menu(self)
|
||||||
|
|
||||||
|
# Меню "Файл"
|
||||||
|
file_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
file_menu.add_command(label="Сохранить", command=self._save_config_changes, accelerator="Ctrl+S")
|
||||||
|
file_menu.add_separator()
|
||||||
|
file_menu.add_command(label="Настройки", command=self._show_settings)
|
||||||
|
file_menu.add_separator()
|
||||||
|
file_menu.add_command(label="Выход", command=self.quit)
|
||||||
|
menubar.add_cascade(label="Файл", menu=file_menu)
|
||||||
|
|
||||||
|
# Меню "Устройство"
|
||||||
|
device_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
device_menu.add_command(label="Подключить", command=self._connect_device)
|
||||||
|
device_menu.add_command(label="Отключить", command=self._disconnect_device)
|
||||||
|
device_menu.add_separator()
|
||||||
|
device_menu.add_command(label="Обновить информацию", command=self._update_device_info)
|
||||||
|
menubar.add_cascade(label="Устройство", menu=device_menu)
|
||||||
|
|
||||||
|
# Меню "Передача"
|
||||||
|
transfer_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
transfer_menu.add_command(label="Передать конфигурацию", command=self._transfer_config)
|
||||||
|
transfer_menu.add_command(label="Отменить передачу", command=self._cancel_transfer)
|
||||||
|
transfer_menu.add_separator()
|
||||||
|
transfer_menu.add_command(label="Запустить TFTP сервер", command=self._start_tftp_server)
|
||||||
|
transfer_menu.add_command(label="Остановить TFTP сервер", command=self._stop_tftp_server)
|
||||||
|
menubar.add_cascade(label="Передача", menu=transfer_menu)
|
||||||
|
|
||||||
|
# Меню "Справка"
|
||||||
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
help_menu.add_command(label="О программе", command=self._show_about)
|
||||||
|
menubar.add_cascade(label="Справка", menu=help_menu)
|
||||||
|
|
||||||
|
self.config(menu=menubar)
|
||||||
|
|
||||||
|
def _create_toolbar(self) -> None:
|
||||||
|
"""Создание панели инструментов."""
|
||||||
|
toolbar = ttk.Frame(self)
|
||||||
|
toolbar.pack(fill=tk.X, padx=5, pady=2)
|
||||||
|
|
||||||
|
# Кнопка сохранения
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Сохранить",
|
||||||
|
command=self._save_config_changes
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y)
|
||||||
|
|
||||||
|
# Кнопки подключения
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Подключить",
|
||||||
|
command=self._connect_device
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Отключить",
|
||||||
|
command=self._disconnect_device
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y)
|
||||||
|
|
||||||
|
# Кнопки передачи
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Передать",
|
||||||
|
command=self._transfer_config
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Отменить",
|
||||||
|
command=self._cancel_transfer
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y)
|
||||||
|
|
||||||
|
# Кнопки TFTP сервера
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Запустить TFTP",
|
||||||
|
command=self._start_tftp_server
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="Остановить TFTP",
|
||||||
|
command=self._stop_tftp_server
|
||||||
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
def _setup_event_handlers(self) -> None:
|
||||||
|
"""Настройка обработчиков событий."""
|
||||||
|
# Привязка сохранения к Ctrl+S
|
||||||
|
self.bind("<Control-s>", lambda e: self._save_config_changes())
|
||||||
|
|
||||||
|
# Привязка отслеживания изменений в редакторе
|
||||||
|
self._editor.bind("<<Modified>>", self._on_editor_change)
|
||||||
|
|
||||||
|
# События подключения
|
||||||
|
event_bus.subscribe(EventTypes.CONNECTION_ESTABLISHED, self._on_connection_established)
|
||||||
|
event_bus.subscribe(EventTypes.CONNECTION_LOST, self._on_connection_lost)
|
||||||
|
event_bus.subscribe(EventTypes.CONNECTION_ERROR, self._on_connection_error)
|
||||||
|
|
||||||
|
# События передачи
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_STARTED, self._on_transfer_started)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_COMPLETED, self._on_transfer_completed)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_ERROR, self._on_transfer_error)
|
||||||
|
|
||||||
|
# События конфигурации
|
||||||
|
event_bus.subscribe(EventTypes.CONFIG_LOADED, self._on_config_loaded)
|
||||||
|
event_bus.subscribe(EventTypes.CONFIG_SAVED, self._on_config_saved)
|
||||||
|
event_bus.subscribe(EventTypes.CONFIG_DELETED, self._on_config_deleted)
|
||||||
|
|
||||||
|
# События UI
|
||||||
|
event_bus.subscribe(EventTypes.UI_STATUS_CHANGED, self._on_status_changed)
|
||||||
|
|
||||||
|
# События списка конфигураций
|
||||||
|
self._configs_list.bind("<<ListboxSelect>>", self._on_config_selected)
|
||||||
|
|
||||||
|
def _load_configs(self) -> None:
|
||||||
|
"""Загрузка списка конфигураций."""
|
||||||
|
try:
|
||||||
|
# Получаем список конфигураций
|
||||||
|
configs = self._config_manager.list_configs()
|
||||||
|
|
||||||
|
# Очищаем список
|
||||||
|
self._configs_list.delete(0, tk.END)
|
||||||
|
|
||||||
|
# Добавляем конфигурации в список
|
||||||
|
for config in configs:
|
||||||
|
self._configs_list.insert(tk.END, config["name"])
|
||||||
|
|
||||||
|
self._logger.debug(f"Загружено {len(configs)} конфигураций")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка загрузки конфигураций: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось загрузить список конфигураций")
|
||||||
|
|
||||||
|
def _add_config(self) -> None:
|
||||||
|
"""Добавление новой конфигурации."""
|
||||||
|
try:
|
||||||
|
# Показываем диалог выбора файла
|
||||||
|
file_path = tk.filedialog.askopenfilename(
|
||||||
|
title="Выберите файл конфигурации",
|
||||||
|
filetypes=[
|
||||||
|
("Текстовые файлы", "*.txt"),
|
||||||
|
("Все файлы", "*.*")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем содержимое файла
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Сохраняем в директорию конфигураций
|
||||||
|
config_name = os.path.basename(file_path)
|
||||||
|
self._config_manager.save_config(config_name, content)
|
||||||
|
|
||||||
|
# Обновляем список конфигураций
|
||||||
|
self._load_configs()
|
||||||
|
|
||||||
|
# Выбираем добавленную конфигурацию
|
||||||
|
configs = self._configs_list.get(0, tk.END)
|
||||||
|
if config_name in configs:
|
||||||
|
index = configs.index(config_name)
|
||||||
|
self._configs_list.selection_set(index)
|
||||||
|
self._configs_list.see(index)
|
||||||
|
|
||||||
|
self._logger.info(f"Добавлена конфигурация: {config_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка добавления конфигурации: {e}")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Ошибка",
|
||||||
|
f"Не удалось добавить конфигурацию:\n{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _remove_config(self) -> None:
|
||||||
|
"""Удаление выбранной конфигурации."""
|
||||||
|
selection = self._configs_list.curselection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
|
||||||
|
config_name = self._configs_list.get(selection[0])
|
||||||
|
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Подтверждение",
|
||||||
|
f"Удалить конфигурацию {config_name}?"
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self._config_manager.delete_config(config_name)
|
||||||
|
self._load_configs()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка удаления конфигурации: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось удалить конфигурацию")
|
||||||
|
|
||||||
|
def _show_settings(self) -> None:
|
||||||
|
"""Отображение диалога настроек."""
|
||||||
|
try:
|
||||||
|
# Получаем текущие настройки
|
||||||
|
settings = {
|
||||||
|
"port": self._serial_manager._protocol._port,
|
||||||
|
"baudrate": self._serial_manager._protocol._baudrate,
|
||||||
|
"timeout": self._serial_manager._protocol._timeout,
|
||||||
|
"prompt": self._serial_manager._protocol._prompt,
|
||||||
|
"copy_mode": self._state_manager.get_custom_data("copy_mode", AppConfig.DEFAULT_COPY_MODE),
|
||||||
|
"block_size": self._state_manager.get_custom_data("block_size", AppConfig.DEFAULT_BLOCK_SIZE),
|
||||||
|
"tftp_port": self._tftp_server._port
|
||||||
|
}
|
||||||
|
|
||||||
|
# Создаем и показываем диалог
|
||||||
|
dialog = SettingsDialog(self, settings)
|
||||||
|
self.wait_window(dialog)
|
||||||
|
|
||||||
|
# Если настройки были изменены, применяем их
|
||||||
|
if dialog.settings != settings:
|
||||||
|
self._apply_settings(dialog.settings)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка отображения настроек: {e}")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Ошибка",
|
||||||
|
f"Не удалось открыть настройки:\n{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_about(self) -> None:
|
||||||
|
"""Отображение диалога 'О программе'."""
|
||||||
|
AboutDialog(self)
|
||||||
|
|
||||||
|
def _connect_device(self) -> None:
|
||||||
|
"""Подключение к устройству."""
|
||||||
|
try:
|
||||||
|
self._serial_manager.connect()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка подключения: {e}")
|
||||||
|
messagebox.showerror("Ошибка", f"Не удалось подключиться к устройству: {e}")
|
||||||
|
|
||||||
|
def _disconnect_device(self) -> None:
|
||||||
|
"""Отключение от устройства."""
|
||||||
|
try:
|
||||||
|
self._serial_manager.disconnect()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка отключения: {e}")
|
||||||
|
messagebox.showerror("Ошибка", f"Не удалось отключиться от устройства: {e}")
|
||||||
|
|
||||||
|
def _update_device_info(self) -> None:
|
||||||
|
"""Обновление информации об устройстве."""
|
||||||
|
if not self._serial_manager.is_connected:
|
||||||
|
messagebox.showwarning("Предупреждение", "Нет подключения к устройству")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем информацию об устройстве
|
||||||
|
device_info = self._serial_manager.get_device_info()
|
||||||
|
|
||||||
|
# Обновляем состояние
|
||||||
|
self._state_manager.update_device_info(device_info)
|
||||||
|
|
||||||
|
# Обновляем отображение
|
||||||
|
self._device_info["hostname"].configure(text=device_info.get("hostname", "-"))
|
||||||
|
self._device_info["model"].configure(text=device_info.get("model", "-"))
|
||||||
|
self._device_info["version"].configure(text=device_info.get("version", "-"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка получения информации об устройстве: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось получить информацию об устройстве")
|
||||||
|
|
||||||
|
def _transfer_config(self) -> None:
|
||||||
|
"""Передача конфигурации на устройство."""
|
||||||
|
if not self._serial_manager.is_connected:
|
||||||
|
messagebox.showwarning("Предупреждение", "Нет подключения к устройству")
|
||||||
|
return
|
||||||
|
|
||||||
|
selection = self._configs_list.curselection()
|
||||||
|
if not selection:
|
||||||
|
messagebox.showwarning("Предупреждение", "Не выбрана конфигурация")
|
||||||
|
return
|
||||||
|
|
||||||
|
config_name = self._configs_list.get(selection[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем путь к файлу конфигурации
|
||||||
|
config_info = self._config_manager.get_config_info(config_name)
|
||||||
|
if not config_info:
|
||||||
|
raise ValueError("Конфигурация не найдена")
|
||||||
|
|
||||||
|
# Запускаем передачу
|
||||||
|
self._transfer_manager.transfer_config(config_info["path"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка передачи конфигурации: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось передать конфигурацию")
|
||||||
|
|
||||||
|
def _cancel_transfer(self) -> None:
|
||||||
|
"""Отмена передачи конфигурации."""
|
||||||
|
try:
|
||||||
|
self._transfer_manager.cancel_all_transfers()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка отмены передачи: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось отменить передачу")
|
||||||
|
|
||||||
|
def _start_tftp_server(self) -> None:
|
||||||
|
"""Запуск TFTP сервера."""
|
||||||
|
try:
|
||||||
|
self._tftp_server.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка запуска TFTP сервера: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось запустить TFTP сервер")
|
||||||
|
|
||||||
|
def _stop_tftp_server(self) -> None:
|
||||||
|
"""Остановка TFTP сервера."""
|
||||||
|
try:
|
||||||
|
self._tftp_server.stop()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка остановки TFTP сервера: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось остановить TFTP сервер")
|
||||||
|
|
||||||
|
def _on_config_selected(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка выбора конфигурации в списке.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие выбора
|
||||||
|
"""
|
||||||
|
if self._has_unsaved_changes:
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Несохраненные изменения",
|
||||||
|
"Есть несохраненные изменения. Сохранить?"
|
||||||
|
):
|
||||||
|
self._save_config_changes()
|
||||||
|
|
||||||
|
selection = self._configs_list.curselection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
|
||||||
|
config_name = self._configs_list.get(selection[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Загружаем содержимое конфигурации
|
||||||
|
content = self._config_manager.load_config(config_name)
|
||||||
|
|
||||||
|
# Обновляем редактор
|
||||||
|
self._editor.set_text(content)
|
||||||
|
|
||||||
|
# Обновляем состояние
|
||||||
|
self._state_manager.set_current_config(config_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка загрузки конфигурации: {e}")
|
||||||
|
messagebox.showerror("Ошибка", "Не удалось загрузить конфигурацию")
|
||||||
|
|
||||||
|
def _on_connection_established(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка установки соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие подключения
|
||||||
|
"""
|
||||||
|
self._update_device_info()
|
||||||
|
|
||||||
|
def _on_connection_lost(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка потери соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие отключения
|
||||||
|
"""
|
||||||
|
# Очищаем информацию об устройстве
|
||||||
|
self._device_info["hostname"].configure(text="-")
|
||||||
|
self._device_info["model"].configure(text="-")
|
||||||
|
self._device_info["version"].configure(text="-")
|
||||||
|
|
||||||
|
# Очищаем состояние
|
||||||
|
self._state_manager.clear_device_info()
|
||||||
|
|
||||||
|
def _on_connection_error(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка ошибки соединения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие ошибки
|
||||||
|
"""
|
||||||
|
messagebox.showerror("Ошибка", str(event.data))
|
||||||
|
|
||||||
|
def _on_transfer_started(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка начала передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие начала передачи
|
||||||
|
"""
|
||||||
|
# Блокируем редактор
|
||||||
|
self._editor.set_readonly(True)
|
||||||
|
|
||||||
|
def _on_transfer_completed(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка завершения передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие завершения передачи
|
||||||
|
"""
|
||||||
|
# Разблокируем редактор
|
||||||
|
self._editor.set_readonly(False)
|
||||||
|
|
||||||
|
def _on_transfer_error(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка ошибки передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие ошибки
|
||||||
|
"""
|
||||||
|
# Разблокируем редактор
|
||||||
|
self._editor.set_readonly(False)
|
||||||
|
|
||||||
|
messagebox.showerror("Ошибка", str(event.data))
|
||||||
|
|
||||||
|
def _on_config_loaded(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка загрузки конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие загрузки
|
||||||
|
"""
|
||||||
|
self._state_manager.update_config_info(event.data)
|
||||||
|
|
||||||
|
def _on_config_saved(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка сохранения конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие сохранения
|
||||||
|
"""
|
||||||
|
self._state_manager.update_config_info(event.data)
|
||||||
|
self._load_configs()
|
||||||
|
|
||||||
|
def _on_config_deleted(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка удаления конфигурации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие удаления
|
||||||
|
"""
|
||||||
|
self._state_manager.remove_config_info(event.data["name"])
|
||||||
|
self._load_configs()
|
||||||
|
|
||||||
|
def _on_status_changed(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка изменения статуса.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие изменения статуса
|
||||||
|
"""
|
||||||
|
if isinstance(event.data, dict):
|
||||||
|
message = event.data.get("message", "")
|
||||||
|
else:
|
||||||
|
message = str(event.data)
|
||||||
|
|
||||||
|
self._status_bar.configure(text=message)
|
||||||
|
|
||||||
|
def _on_editor_change(self, event=None) -> None:
|
||||||
|
"""
|
||||||
|
Обработка изменений в редакторе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие изменения
|
||||||
|
"""
|
||||||
|
if not self._has_unsaved_changes:
|
||||||
|
self._has_unsaved_changes = True
|
||||||
|
self.title(f"*ComConfigCopy v{AppConfig.VERSION}")
|
||||||
|
|
||||||
|
def _save_config_changes(self) -> None:
|
||||||
|
"""Сохранение изменений в редакторе конфигурации."""
|
||||||
|
try:
|
||||||
|
# Получаем текущую конфигурацию
|
||||||
|
selection = self._configs_list.curselection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
|
||||||
|
config_name = self._configs_list.get(selection[0])
|
||||||
|
|
||||||
|
# Получаем содержимое редактора
|
||||||
|
content = self._editor.get_text()
|
||||||
|
|
||||||
|
# Сохраняем изменения
|
||||||
|
self._config_manager.save_config(config_name, content)
|
||||||
|
|
||||||
|
# Сбрасываем флаг изменений
|
||||||
|
self._has_unsaved_changes = False
|
||||||
|
self.title(f"ComConfigCopy v{AppConfig.VERSION}")
|
||||||
|
|
||||||
|
self._logger.info(f"Сохранены изменения в конфигурации: {config_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка сохранения изменений: {e}")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Ошибка",
|
||||||
|
f"Не удалось сохранить изменения:\n{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_settings(self, settings: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Применение новых настроек.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
settings: Словарь с настройками
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Настраиваем последовательный порт
|
||||||
|
self._serial_manager._protocol.configure(
|
||||||
|
port=settings["port"],
|
||||||
|
baudrate=settings["baudrate"],
|
||||||
|
timeout=settings["timeout"],
|
||||||
|
prompt=settings["prompt"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Настраиваем TFTP сервер
|
||||||
|
self._tftp_server.configure(port=settings["tftp_port"])
|
||||||
|
|
||||||
|
# Сохраняем настройки передачи
|
||||||
|
self._state_manager.set_custom_data("copy_mode", settings["copy_mode"])
|
||||||
|
self._state_manager.set_custom_data("block_size", settings["block_size"])
|
||||||
|
|
||||||
|
self._logger.info("Настройки применены")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"Ошибка применения настроек: {e}")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Ошибка",
|
||||||
|
f"Не удалось применить настройки:\n{str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_close(self) -> None:
|
||||||
|
"""Обработка закрытия окна."""
|
||||||
|
if self._has_unsaved_changes:
|
||||||
|
answer = messagebox.askyesnocancel(
|
||||||
|
"Несохраненные изменения",
|
||||||
|
"Есть несохраненные изменения. Сохранить перед выходом?"
|
||||||
|
)
|
||||||
|
|
||||||
|
if answer is None: # Отмена
|
||||||
|
return
|
||||||
|
elif answer: # Да
|
||||||
|
self._save_config_changes()
|
||||||
|
|
||||||
|
# Останавливаем компоненты
|
||||||
|
self._config_watcher.stop()
|
||||||
|
self._tftp_server.stop()
|
||||||
|
self._serial_manager.disconnect()
|
||||||
|
|
||||||
|
# Закрываем окно
|
||||||
|
self.quit()
|
||||||
0
src/ui/widgets/__init__.py
Normal file
0
src/ui/widgets/__init__.py
Normal file
200
src/ui/widgets/custom_entry.py
Normal file
200
src/ui/widgets/custom_entry.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Optional, Callable, Any
|
||||||
|
import re
|
||||||
|
|
||||||
|
class CustomEntry(ttk.Frame):
|
||||||
|
"""Пользовательское поле ввода с дополнительными возможностями."""
|
||||||
|
|
||||||
|
def __init__(self, master, validator: Optional[Callable[[str], bool]] = None,
|
||||||
|
placeholder: Optional[str] = None, **kwargs):
|
||||||
|
super().__init__(master)
|
||||||
|
|
||||||
|
# Создаем поле ввода
|
||||||
|
self.entry = ttk.Entry(self, **kwargs)
|
||||||
|
self.entry.pack(fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
# Функция валидации
|
||||||
|
self._validator = validator
|
||||||
|
|
||||||
|
# Текст подсказки
|
||||||
|
self._placeholder = placeholder
|
||||||
|
self._placeholder_color = "gray"
|
||||||
|
self._default_fg = self.entry["foreground"]
|
||||||
|
|
||||||
|
# Флаг показа пароля
|
||||||
|
self._show_password = False
|
||||||
|
self._default_show = self.entry.cget("show")
|
||||||
|
|
||||||
|
# Привязываем события
|
||||||
|
self.entry.bind("<FocusIn>", self._on_focus_in)
|
||||||
|
self.entry.bind("<FocusOut>", self._on_focus_out)
|
||||||
|
self.entry.bind("<KeyRelease>", self._on_key_release)
|
||||||
|
|
||||||
|
# Добавляем контекстное меню
|
||||||
|
self._create_context_menu()
|
||||||
|
self.entry.bind("<Button-3>", self._show_context_menu)
|
||||||
|
|
||||||
|
# Устанавливаем подсказку, если указана
|
||||||
|
if placeholder:
|
||||||
|
self._show_placeholder()
|
||||||
|
|
||||||
|
def _create_context_menu(self) -> None:
|
||||||
|
"""Создание контекстного меню."""
|
||||||
|
self.context_menu = tk.Menu(self, tearoff=0)
|
||||||
|
self.context_menu.add_command(label="Копировать", command=self.copy)
|
||||||
|
self.context_menu.add_command(label="Вставить", command=self.paste)
|
||||||
|
self.context_menu.add_command(label="Вырезать", command=self.cut)
|
||||||
|
self.context_menu.add_separator()
|
||||||
|
self.context_menu.add_command(label="Выделить всё", command=self.select_all)
|
||||||
|
|
||||||
|
def _show_context_menu(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Отображение контекстного меню.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие мыши
|
||||||
|
"""
|
||||||
|
state = tk.NORMAL if not self.entry.cget("state") == tk.DISABLED else tk.DISABLED
|
||||||
|
self.context_menu.entryconfig("Вставить", state=state)
|
||||||
|
self.context_menu.entryconfig("Вырезать", state=state)
|
||||||
|
|
||||||
|
self.context_menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
|
||||||
|
def _on_focus_in(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка получения фокуса.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие фокуса
|
||||||
|
"""
|
||||||
|
if self._placeholder and self.entry.get() == self._placeholder:
|
||||||
|
self.entry.delete(0, tk.END)
|
||||||
|
self.entry.configure(foreground=self._default_fg)
|
||||||
|
|
||||||
|
def _on_focus_out(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка потери фокуса.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие фокуса
|
||||||
|
"""
|
||||||
|
if self._placeholder and not self.entry.get():
|
||||||
|
self._show_placeholder()
|
||||||
|
|
||||||
|
def _on_key_release(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка отпускания клавиши.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие клавиатуры
|
||||||
|
"""
|
||||||
|
if self._validator:
|
||||||
|
text = self.entry.get()
|
||||||
|
if not self._validator(text):
|
||||||
|
self.entry.configure(foreground="red")
|
||||||
|
else:
|
||||||
|
self.entry.configure(foreground=self._default_fg)
|
||||||
|
|
||||||
|
def _show_placeholder(self) -> None:
|
||||||
|
"""Отображение текста подсказки."""
|
||||||
|
self.entry.delete(0, tk.END)
|
||||||
|
self.entry.insert(0, self._placeholder)
|
||||||
|
self.entry.configure(foreground=self._placeholder_color)
|
||||||
|
|
||||||
|
def get(self) -> str:
|
||||||
|
"""
|
||||||
|
Получение текста.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Текст поля ввода
|
||||||
|
"""
|
||||||
|
text = self.entry.get()
|
||||||
|
if self._placeholder and text == self._placeholder:
|
||||||
|
return ""
|
||||||
|
return text
|
||||||
|
|
||||||
|
def set(self, text: str) -> None:
|
||||||
|
"""
|
||||||
|
Установка текста в поле ввода.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст для установки
|
||||||
|
"""
|
||||||
|
self.entry.delete(0, tk.END)
|
||||||
|
if text:
|
||||||
|
self.entry.insert(0, text)
|
||||||
|
self.entry.configure(foreground=self._default_fg)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Очистка текста."""
|
||||||
|
self.entry.delete(0, tk.END)
|
||||||
|
if self._placeholder:
|
||||||
|
self._show_placeholder()
|
||||||
|
|
||||||
|
def copy(self) -> None:
|
||||||
|
"""Копирование текста."""
|
||||||
|
self.entry.event_generate("<<Copy>>")
|
||||||
|
|
||||||
|
def paste(self) -> None:
|
||||||
|
"""Вставка текста."""
|
||||||
|
self.entry.event_generate("<<Paste>>")
|
||||||
|
|
||||||
|
def cut(self) -> None:
|
||||||
|
"""Вырезание текста."""
|
||||||
|
self.entry.event_generate("<<Cut>>")
|
||||||
|
|
||||||
|
def select_all(self) -> None:
|
||||||
|
"""Выделение всего текста."""
|
||||||
|
self.entry.select_range(0, tk.END)
|
||||||
|
self.entry.icursor(tk.END)
|
||||||
|
|
||||||
|
def set_validator(self, validator: Optional[Callable[[str], bool]]) -> None:
|
||||||
|
"""
|
||||||
|
Установка функции валидации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
validator: Функция валидации
|
||||||
|
"""
|
||||||
|
self._validator = validator
|
||||||
|
|
||||||
|
def set_placeholder(self, text: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
Установка текста подсказки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст подсказки
|
||||||
|
"""
|
||||||
|
self._placeholder = text
|
||||||
|
if not self.entry.get():
|
||||||
|
self._show_placeholder()
|
||||||
|
|
||||||
|
def toggle_password(self) -> None:
|
||||||
|
"""Переключение отображения пароля."""
|
||||||
|
self._show_password = not self._show_password
|
||||||
|
if self._show_password:
|
||||||
|
self.entry.configure(show="")
|
||||||
|
else:
|
||||||
|
self.entry.configure(show=self._default_show)
|
||||||
|
|
||||||
|
def set_state(self, state: str) -> None:
|
||||||
|
"""
|
||||||
|
Установка состояния поля ввода.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: Состояние (tk.NORMAL или tk.DISABLED)
|
||||||
|
"""
|
||||||
|
self.entry.configure(state=state)
|
||||||
|
|
||||||
|
def bind_key(self, key: str, callback: Callable) -> None:
|
||||||
|
"""
|
||||||
|
Привязка обработчика к клавише.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Клавиша или комбинация клавиш
|
||||||
|
callback: Функция обработчик
|
||||||
|
"""
|
||||||
|
self.entry.bind(key, callback)
|
||||||
197
src/ui/widgets/custom_text.py
Normal file
197
src/ui/widgets/custom_text.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Optional, Callable
|
||||||
|
import re
|
||||||
|
|
||||||
|
class CustomText(ttk.Frame):
|
||||||
|
"""Пользовательский текстовый виджет с дополнительными возможностями."""
|
||||||
|
|
||||||
|
def __init__(self, master, **kwargs):
|
||||||
|
super().__init__(master)
|
||||||
|
|
||||||
|
# Создаем текстовый виджет
|
||||||
|
self.text = tk.Text(
|
||||||
|
self,
|
||||||
|
wrap=tk.WORD,
|
||||||
|
undo=True,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем скроллбар
|
||||||
|
self.scrollbar = ttk.Scrollbar(
|
||||||
|
self,
|
||||||
|
orient=tk.VERTICAL,
|
||||||
|
command=self.text.yview
|
||||||
|
)
|
||||||
|
|
||||||
|
# Настраиваем прокрутку
|
||||||
|
self.text.configure(yscrollcommand=self.scrollbar.set)
|
||||||
|
|
||||||
|
# Размещаем виджеты
|
||||||
|
self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
# Настраиваем теги
|
||||||
|
self.text.tag_configure("error", foreground="red")
|
||||||
|
self.text.tag_configure("success", foreground="green")
|
||||||
|
self.text.tag_configure("warning", foreground="orange")
|
||||||
|
self.text.tag_configure("info", foreground="blue")
|
||||||
|
|
||||||
|
# Добавляем контекстное меню
|
||||||
|
self._create_context_menu()
|
||||||
|
|
||||||
|
# Привязываем события
|
||||||
|
self.text.bind("<Button-3>", self._show_context_menu)
|
||||||
|
|
||||||
|
# Флаг только для чтения
|
||||||
|
self._readonly = False
|
||||||
|
|
||||||
|
def _create_context_menu(self) -> None:
|
||||||
|
"""Создание контекстного меню."""
|
||||||
|
self.context_menu = tk.Menu(self, tearoff=0)
|
||||||
|
self.context_menu.add_command(label="Копировать", command=self.copy)
|
||||||
|
self.context_menu.add_command(label="Вставить", command=self.paste)
|
||||||
|
self.context_menu.add_command(label="Вырезать", command=self.cut)
|
||||||
|
self.context_menu.add_separator()
|
||||||
|
self.context_menu.add_command(label="Выделить всё", command=self.select_all)
|
||||||
|
|
||||||
|
def _show_context_menu(self, event) -> None:
|
||||||
|
"""
|
||||||
|
Отображение контекстного меню.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие мыши
|
||||||
|
"""
|
||||||
|
if self._readonly:
|
||||||
|
# В режиме только для чтения оставляем только копирование
|
||||||
|
self.context_menu.entryconfig("Вставить", state=tk.DISABLED)
|
||||||
|
self.context_menu.entryconfig("Вырезать", state=tk.DISABLED)
|
||||||
|
else:
|
||||||
|
self.context_menu.entryconfig("Вставить", state=tk.NORMAL)
|
||||||
|
self.context_menu.entryconfig("Вырезать", state=tk.NORMAL)
|
||||||
|
|
||||||
|
self.context_menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
|
||||||
|
def get_text(self) -> str:
|
||||||
|
"""
|
||||||
|
Получение текста.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Текст виджета
|
||||||
|
"""
|
||||||
|
return self.text.get("1.0", tk.END).strip()
|
||||||
|
|
||||||
|
def set_text(self, content: str) -> None:
|
||||||
|
"""
|
||||||
|
Установка текста.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Текст для установки
|
||||||
|
"""
|
||||||
|
self.clear()
|
||||||
|
self.text.insert("1.0", content)
|
||||||
|
|
||||||
|
def append_text(self, content: str, tag: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Добавление текста.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Текст для добавления
|
||||||
|
tag: Тег форматирования
|
||||||
|
"""
|
||||||
|
self.text.insert(tk.END, content + "\n", tag)
|
||||||
|
self.text.see(tk.END)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Очистка текста."""
|
||||||
|
self.text.delete("1.0", tk.END)
|
||||||
|
|
||||||
|
def copy(self) -> None:
|
||||||
|
"""Копирование выделенного текста."""
|
||||||
|
self.text.event_generate("<<Copy>>")
|
||||||
|
|
||||||
|
def paste(self) -> None:
|
||||||
|
"""Вставка текста."""
|
||||||
|
if not self._readonly:
|
||||||
|
self.text.event_generate("<<Paste>>")
|
||||||
|
|
||||||
|
def cut(self) -> None:
|
||||||
|
"""Вырезание выделенного текста."""
|
||||||
|
if not self._readonly:
|
||||||
|
self.text.event_generate("<<Cut>>")
|
||||||
|
|
||||||
|
def select_all(self) -> None:
|
||||||
|
"""Выделение всего текста."""
|
||||||
|
self.text.tag_add(tk.SEL, "1.0", tk.END)
|
||||||
|
self.text.mark_set(tk.INSERT, "1.0")
|
||||||
|
self.text.see(tk.INSERT)
|
||||||
|
|
||||||
|
def highlight_pattern(self, pattern: str, tag: str) -> None:
|
||||||
|
"""
|
||||||
|
Подсветка текста по шаблону.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pattern: Регулярное выражение
|
||||||
|
tag: Тег форматирования
|
||||||
|
"""
|
||||||
|
self.text.tag_remove(tag, "1.0", tk.END)
|
||||||
|
|
||||||
|
start = "1.0"
|
||||||
|
while True:
|
||||||
|
start = self.text.search(pattern, start, tk.END, regexp=True)
|
||||||
|
if not start:
|
||||||
|
break
|
||||||
|
|
||||||
|
end = f"{start}+{len(pattern)}c"
|
||||||
|
self.text.tag_add(tag, start, end)
|
||||||
|
start = end
|
||||||
|
|
||||||
|
def set_readonly(self, readonly: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Установка режима только для чтения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
readonly: Флаг режима
|
||||||
|
"""
|
||||||
|
self._readonly = readonly
|
||||||
|
state = tk.DISABLED if readonly else tk.NORMAL
|
||||||
|
self.text.configure(state=state)
|
||||||
|
|
||||||
|
def bind_key(self, key: str, callback: Callable) -> None:
|
||||||
|
"""
|
||||||
|
Привязка обработчика к клавише.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Клавиша или комбинация клавиш
|
||||||
|
callback: Функция обработчик
|
||||||
|
"""
|
||||||
|
self.text.bind(key, callback)
|
||||||
|
|
||||||
|
def get_line(self, line_number: int) -> str:
|
||||||
|
"""
|
||||||
|
Получение строки по номеру.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line_number: Номер строки (1-based)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Текст строки
|
||||||
|
"""
|
||||||
|
start = f"{line_number}.0"
|
||||||
|
end = f"{line_number}.end"
|
||||||
|
return self.text.get(start, end)
|
||||||
|
|
||||||
|
def get_selection(self) -> str:
|
||||||
|
"""
|
||||||
|
Получение выделенного текста.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Выделенный текст
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.text.get(tk.SEL_FIRST, tk.SEL_LAST)
|
||||||
|
except tk.TclError:
|
||||||
|
return ""
|
||||||
233
src/ui/widgets/transfer_view.py
Normal file
233
src/ui/widgets/transfer_view.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import time
|
||||||
|
|
||||||
|
from core.events import event_bus, Event, EventTypes
|
||||||
|
|
||||||
|
class TransferView(ttk.Frame):
|
||||||
|
"""Виджет для отображения прогресса передачи файлов."""
|
||||||
|
|
||||||
|
def __init__(self, master):
|
||||||
|
super().__init__(master)
|
||||||
|
|
||||||
|
# Создаем элементы интерфейса
|
||||||
|
self._create_widgets()
|
||||||
|
|
||||||
|
# Подписываемся на события
|
||||||
|
self._setup_event_handlers()
|
||||||
|
|
||||||
|
# Состояние передачи
|
||||||
|
self._transfer_id: Optional[str] = None
|
||||||
|
self._start_time: float = 0
|
||||||
|
self._total_bytes: int = 0
|
||||||
|
self._transferred_bytes: int = 0
|
||||||
|
|
||||||
|
def _create_widgets(self) -> None:
|
||||||
|
"""Создание элементов интерфейса."""
|
||||||
|
# Заголовок
|
||||||
|
self.title_label = ttk.Label(
|
||||||
|
self,
|
||||||
|
text="Прогресс передачи",
|
||||||
|
font=("Arial", 10, "bold")
|
||||||
|
)
|
||||||
|
self.title_label.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Имя файла
|
||||||
|
self.file_frame = ttk.Frame(self)
|
||||||
|
self.file_frame.pack(fill=tk.X, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(self.file_frame, text="Файл:").pack(side=tk.LEFT)
|
||||||
|
self.file_label = ttk.Label(self.file_frame, text="-")
|
||||||
|
self.file_label.pack(side=tk.LEFT, padx=(5, 0))
|
||||||
|
|
||||||
|
# Прогресс-бар
|
||||||
|
self.progress = ttk.Progressbar(
|
||||||
|
self,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
mode="determinate",
|
||||||
|
length=200
|
||||||
|
)
|
||||||
|
self.progress.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# Процент выполнения
|
||||||
|
self.percent_label = ttk.Label(self, text="0%")
|
||||||
|
self.percent_label.pack()
|
||||||
|
|
||||||
|
# Скорость передачи
|
||||||
|
self.speed_frame = ttk.Frame(self)
|
||||||
|
self.speed_frame.pack(fill=tk.X, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(self.speed_frame, text="Скорость:").pack(side=tk.LEFT)
|
||||||
|
self.speed_label = ttk.Label(self.speed_frame, text="-")
|
||||||
|
self.speed_label.pack(side=tk.LEFT, padx=(5, 0))
|
||||||
|
|
||||||
|
# Оставшееся время
|
||||||
|
self.time_frame = ttk.Frame(self)
|
||||||
|
self.time_frame.pack(fill=tk.X, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(self.time_frame, text="Осталось:").pack(side=tk.LEFT)
|
||||||
|
self.time_label = ttk.Label(self.time_frame, text="-")
|
||||||
|
self.time_label.pack(side=tk.LEFT, padx=(5, 0))
|
||||||
|
|
||||||
|
# Статус
|
||||||
|
self.status_label = ttk.Label(
|
||||||
|
self,
|
||||||
|
text="Готов к передаче",
|
||||||
|
font=("Arial", 9, "italic")
|
||||||
|
)
|
||||||
|
self.status_label.pack(pady=5)
|
||||||
|
|
||||||
|
def _setup_event_handlers(self) -> None:
|
||||||
|
"""Настройка обработчиков событий."""
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_STARTED, self._handle_transfer_started)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_PROGRESS, self._handle_transfer_progress)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_COMPLETED, self._handle_transfer_completed)
|
||||||
|
event_bus.subscribe(EventTypes.TRANSFER_ERROR, self._handle_transfer_error)
|
||||||
|
|
||||||
|
def _handle_transfer_started(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка начала передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие начала передачи
|
||||||
|
"""
|
||||||
|
data = event.data
|
||||||
|
self._transfer_id = data.get("transfer_id")
|
||||||
|
self._total_bytes = data.get("total_bytes", 0)
|
||||||
|
self._transferred_bytes = 0
|
||||||
|
self._start_time = time.time()
|
||||||
|
|
||||||
|
# Обновляем интерфейс
|
||||||
|
self.file_label.configure(text=data.get("file", "-"))
|
||||||
|
self.progress.configure(value=0)
|
||||||
|
self.percent_label.configure(text="0%")
|
||||||
|
self.speed_label.configure(text="-")
|
||||||
|
self.time_label.configure(text="-")
|
||||||
|
self.status_label.configure(text="Передача начата...")
|
||||||
|
|
||||||
|
def _handle_transfer_progress(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка прогресса передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие прогресса
|
||||||
|
"""
|
||||||
|
data = event.data
|
||||||
|
if data.get("transfer_id") != self._transfer_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновляем прогресс
|
||||||
|
progress = data.get("progress", 0)
|
||||||
|
self.progress.configure(value=progress)
|
||||||
|
self.percent_label.configure(text=f"{progress:.1f}%")
|
||||||
|
|
||||||
|
# Обновляем скорость
|
||||||
|
speed = data.get("speed", 0)
|
||||||
|
self.speed_label.configure(text=self._format_speed(speed))
|
||||||
|
|
||||||
|
# Обновляем оставшееся время
|
||||||
|
time_left = data.get("estimated_time", 0)
|
||||||
|
self.time_label.configure(text=self._format_time(time_left))
|
||||||
|
|
||||||
|
# Обновляем статус
|
||||||
|
self.status_label.configure(text="Передача данных...")
|
||||||
|
|
||||||
|
def _handle_transfer_completed(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка завершения передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие завершения
|
||||||
|
"""
|
||||||
|
data = event.data
|
||||||
|
if data.get("transfer_id") != self._transfer_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновляем прогресс
|
||||||
|
self.progress.configure(value=100)
|
||||||
|
self.percent_label.configure(text="100%")
|
||||||
|
|
||||||
|
# Обновляем статус
|
||||||
|
total_time = data.get("total_time", 0)
|
||||||
|
average_speed = data.get("average_speed", 0)
|
||||||
|
|
||||||
|
status = (
|
||||||
|
f"Передача завершена за {self._format_time(total_time)}\n"
|
||||||
|
f"Средняя скорость: {self._format_speed(average_speed)}"
|
||||||
|
)
|
||||||
|
self.status_label.configure(text=status)
|
||||||
|
|
||||||
|
# Сбрасываем состояние
|
||||||
|
self._transfer_id = None
|
||||||
|
|
||||||
|
def _handle_transfer_error(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
Обработка ошибки передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Событие ошибки
|
||||||
|
"""
|
||||||
|
data = event.data
|
||||||
|
if data.get("transfer_id") != self._transfer_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновляем статус
|
||||||
|
error = data.get("error", "Неизвестная ошибка")
|
||||||
|
self.status_label.configure(text=f"Ошибка: {error}")
|
||||||
|
|
||||||
|
# Сбрасываем состояние
|
||||||
|
self._transfer_id = None
|
||||||
|
|
||||||
|
def _format_speed(self, speed: float) -> str:
|
||||||
|
"""
|
||||||
|
Форматирование скорости передачи.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
speed: Скорость в байтах в секунду
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка
|
||||||
|
"""
|
||||||
|
if speed < 1024:
|
||||||
|
return f"{speed:.1f} Б/с"
|
||||||
|
elif speed < 1024 * 1024:
|
||||||
|
return f"{speed/1024:.1f} КБ/с"
|
||||||
|
else:
|
||||||
|
return f"{speed/1024/1024:.1f} МБ/с"
|
||||||
|
|
||||||
|
def _format_time(self, seconds: float) -> str:
|
||||||
|
"""
|
||||||
|
Форматирование времени.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds: Время в секундах
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка
|
||||||
|
"""
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.1f} сек"
|
||||||
|
elif seconds < 3600:
|
||||||
|
minutes = seconds / 60
|
||||||
|
return f"{minutes:.1f} мин"
|
||||||
|
else:
|
||||||
|
hours = seconds / 3600
|
||||||
|
return f"{hours:.1f} ч"
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Сброс состояния виджета."""
|
||||||
|
self._transfer_id = None
|
||||||
|
self._start_time = 0
|
||||||
|
self._total_bytes = 0
|
||||||
|
self._transferred_bytes = 0
|
||||||
|
|
||||||
|
self.file_label.configure(text="-")
|
||||||
|
self.progress.configure(value=0)
|
||||||
|
self.percent_label.configure(text="0%")
|
||||||
|
self.speed_label.configure(text="-")
|
||||||
|
self.time_label.configure(text="-")
|
||||||
|
self.status_label.configure(text="Готов к передаче")
|
||||||
0
src/utils/decorators/__init__.py
Normal file
0
src/utils/decorators/__init__.py
Normal file
0
src/utils/decorators/logging.py
Normal file
0
src/utils/decorators/logging.py
Normal file
0
src/utils/decorators/performance.py
Normal file
0
src/utils/decorators/performance.py
Normal file
0
src/utils/formatters.py
Normal file
0
src/utils/formatters.py
Normal file
0
src/utils/updater/__init__.py
Normal file
0
src/utils/updater/__init__.py
Normal file
0
src/utils/updater/update_manager.py
Normal file
0
src/utils/updater/update_manager.py
Normal file
0
src/utils/updater/version_checker.py
Normal file
0
src/utils/updater/version_checker.py
Normal file
0
src/utils/validators.py
Normal file
0
src/utils/validators.py
Normal file
Reference in New Issue
Block a user