Compare commits
6 Commits
v1.1
...
f5935d6b8f
| Author | SHA1 | Date | |
|---|---|---|---|
| f5935d6b8f | |||
| 1a511ff54f | |||
| f84e20631b | |||
| 4a67e70a92 | |||
| 12562e615f | |||
| 7ebeb52808 |
1287
ComConfigCopy.py
Normal file
1287
ComConfigCopy.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
# ComConfigCopy
|
||||
|
||||
Исходный код для версии 1.1. Работоспособность не гарантирована, работа ведётся.
|
||||
|
||||
Программа для копирования конфигураций на коммутаторы.
|
||||
|
||||
## Описание
|
||||
@@ -37,7 +35,7 @@ ComConfigCopy - это утилита, разработанная для авт
|
||||
|
||||
## Контакты
|
||||
|
||||
- Email: LowaWorkMail@gmail.com
|
||||
- Email: SPRF555@gmail.com
|
||||
- Telegram: [@LowaSC](https://t.me/LowaSC)
|
||||
- Репозиторий: [ComConfigCopy](https://gitea.filow.ru/LowaSC/ComConfigCopy)
|
||||
|
||||
|
||||
349
TFTPServer.py
Normal file
349
TFTPServer.py
Normal file
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TFTP сервер для передачи прошивки с компьютера на коммутатор.
|
||||
|
||||
- Создает сервер по заданному IP и порту.
|
||||
- Расшаривает папку Firmware.
|
||||
- Показывает текущее состояние сервера и статус передачи файла:
|
||||
- кому (IP устройства),
|
||||
- сколько осталось байт,
|
||||
- сколько передано байт,
|
||||
- время передачи.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
class TFTPServer:
|
||||
def __init__(self, share_folder):
|
||||
"""
|
||||
Инициализация TFTP сервера.
|
||||
|
||||
:param share_folder: Путь к папке, содержащей файлы (например, папка 'Firmware')
|
||||
"""
|
||||
self.share_folder = share_folder
|
||||
self.log_callback = None
|
||||
self.running = False
|
||||
self.server_socket = None
|
||||
self.lock = threading.Lock()
|
||||
self.transfer_sockets = set() # Множество для хранения всех активных сокетов передачи
|
||||
# Словарь активных передач для мониторинга их статуса.
|
||||
# Ключ – адрес клиента, значение – словарь с информацией о передаче.
|
||||
self.active_transfers = {}
|
||||
|
||||
def set_log_callback(self, callback):
|
||||
"""
|
||||
Установка функции обратного вызова для логирования сообщений.
|
||||
|
||||
:param callback: Функция, принимающая строку сообщения.
|
||||
"""
|
||||
self.log_callback = callback
|
||||
|
||||
def log(self, message):
|
||||
"""
|
||||
Функция логирования: вызывает callback (если задан) или выводит сообщение в консоль.
|
||||
|
||||
:param message: Строка с сообщением для логирования.
|
||||
"""
|
||||
if self.log_callback:
|
||||
self.log_callback(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
def start_server(self, ip, port):
|
||||
"""
|
||||
Запуск TFTP сервера на указанном IP и порту.
|
||||
|
||||
:param ip: IP-адрес для привязки сервера.
|
||||
:param port: Порт для TFTP сервера.
|
||||
"""
|
||||
if self.running:
|
||||
self.log("[WARN] Сервер уже запущен")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
try:
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.server_socket.bind((ip, port))
|
||||
self.log(f"[INFO] TFTP сервер запущен на {ip}:{port}")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
self.server_socket.settimeout(1.0)
|
||||
data, client_addr = self.server_socket.recvfrom(2048)
|
||||
if data and self.running:
|
||||
threading.Thread(target=self.handle_request, args=(data, client_addr), daemon=True).start()
|
||||
except socket.timeout:
|
||||
continue
|
||||
except socket.error as e:
|
||||
if self.running: # Логируем ошибку только если сервер еще запущен
|
||||
self.log(f"[ERROR] Ошибка получения данных: {str(e)}")
|
||||
break
|
||||
except Exception as e:
|
||||
if self.running: # Логируем ошибку только если сервер еще запущен
|
||||
self.log(f"[ERROR] Ошибка получения данных: {str(e)}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"[ERROR] Ошибка запуска сервера: {str(e)}")
|
||||
finally:
|
||||
self.running = False
|
||||
if self.server_socket:
|
||||
try:
|
||||
self.server_socket.close()
|
||||
except:
|
||||
pass
|
||||
self.server_socket = None
|
||||
|
||||
def stop_server(self):
|
||||
"""
|
||||
Остановка TFTP сервера.
|
||||
"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.log("[INFO] Остановка TFTP сервера...")
|
||||
self.running = False
|
||||
|
||||
try:
|
||||
# Закрываем основной сокет сервера первым
|
||||
if self.server_socket:
|
||||
try:
|
||||
# Создаем временный сокет и отправляем пакет самому себе,
|
||||
# чтобы разблокировать recvfrom
|
||||
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
server_address = self.server_socket.getsockname()
|
||||
temp_socket.sendto(b'', server_address)
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
temp_socket.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.server_socket.close()
|
||||
except Exception as e:
|
||||
self.log(f"[WARN] Ошибка при закрытии основного сокета: {str(e)}")
|
||||
finally:
|
||||
self.server_socket = None
|
||||
|
||||
# Закрываем все активные сокеты передачи
|
||||
with self.lock:
|
||||
active_sockets = list(self.transfer_sockets)
|
||||
self.transfer_sockets.clear()
|
||||
active_transfers = dict(self.active_transfers)
|
||||
self.active_transfers.clear()
|
||||
|
||||
# Закрываем сокеты передачи после очистки множества
|
||||
for sock in active_sockets:
|
||||
try:
|
||||
if sock:
|
||||
sock.close()
|
||||
except Exception as e:
|
||||
self.log(f"[WARN] Ошибка при закрытии сокета передачи: {str(e)}")
|
||||
|
||||
# Отправляем сообщения об остановке для активных передач
|
||||
for client_addr, transfer_info in active_transfers.items():
|
||||
try:
|
||||
self.send_error(client_addr, 0, "Сервер остановлен")
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"[ERROR] Ошибка при остановке сервера: {str(e)}")
|
||||
finally:
|
||||
self.running = False # Гарантируем, что флаг running будет False
|
||||
self.log("[INFO] TFTP сервер остановлен")
|
||||
|
||||
def handle_request(self, data, client_addr):
|
||||
"""
|
||||
Обработка входящего запроса от клиента.
|
||||
|
||||
:param data: Полученные данные (UDP-пакет).
|
||||
:param client_addr: Адрес клиента, отправившего пакет.
|
||||
"""
|
||||
if len(data) < 2:
|
||||
self.log(f"[WARN] Получен некорректный пакет от {client_addr}")
|
||||
return
|
||||
opcode = struct.unpack("!H", data[:2])[0]
|
||||
if opcode == 1: # RRQ (Read Request) – запрос на чтение файла
|
||||
self.handle_rrq(data, client_addr)
|
||||
else:
|
||||
self.log(f"[WARN] Неподдерживаемый запрос (опкод {opcode}) от {client_addr}")
|
||||
|
||||
def handle_rrq(self, data, client_addr):
|
||||
"""
|
||||
Обработка запроса на чтение файла (RRQ).
|
||||
|
||||
:param data: Данные запроса.
|
||||
:param client_addr: Адрес клиента.
|
||||
"""
|
||||
try:
|
||||
# RRQ формата: 2 байта опкода, затем строка имени файла, за которой следует 0,
|
||||
# затем строка режима (например, "octet"), и завершается 0.
|
||||
parts = data[2:].split(b'\0')
|
||||
if len(parts) < 2:
|
||||
self.log(f"[WARN] Некорректный RRQ пакет от {client_addr}")
|
||||
return
|
||||
filename = parts[0].decode('utf-8')
|
||||
mode = parts[1].decode('utf-8').lower()
|
||||
self.log(f"[INFO] Получен RRQ от {client_addr}: файл '{filename}', режим '{mode}'")
|
||||
if mode != "octet":
|
||||
self.send_error(client_addr, 0, "Поддерживается только octet режим")
|
||||
return
|
||||
file_path = os.path.join(self.share_folder, filename)
|
||||
if not os.path.isfile(file_path):
|
||||
self.send_error(client_addr, 1, "Файл не найден")
|
||||
return
|
||||
# Запускаем передачу файла в новом потоке.
|
||||
threading.Thread(target=self.send_file, args=(file_path, client_addr), daemon=True).start()
|
||||
except Exception as e:
|
||||
self.log(f"[ERROR] Ошибка обработки RRQ: {str(e)}")
|
||||
|
||||
def send_error(self, client_addr, error_code, error_message):
|
||||
"""
|
||||
Отправка сообщения об ошибке клиенту.
|
||||
|
||||
:param client_addr: Адрес клиента.
|
||||
:param error_code: Код ошибки.
|
||||
:param error_message: Текст ошибки.
|
||||
"""
|
||||
# Формируем TFTP пакет ошибки: 2 байта опкода (5), 2 байта кода ошибки, сообщение об ошибке и завершающий 0.
|
||||
packet = struct.pack("!HH", 5, error_code) + error_message.encode('utf-8') + b'\0'
|
||||
self.server_socket.sendto(packet, client_addr)
|
||||
self.log(f"[INFO] Отправлено сообщение об ошибке '{error_message}' клиенту {client_addr}")
|
||||
|
||||
def send_file(self, file_path, client_addr):
|
||||
"""
|
||||
Передача файла клиенту по протоколу TFTP.
|
||||
"""
|
||||
BLOCK_SIZE = 512
|
||||
MAX_RETRIES = 5
|
||||
TIMEOUT = 2.0
|
||||
transfer_socket = None
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
self.log(f"[ERROR] Файл '{file_path}' не существует")
|
||||
self.send_error(client_addr, 1, "Файл не найден")
|
||||
return
|
||||
|
||||
filesize = os.path.getsize(file_path)
|
||||
if filesize == 0:
|
||||
self.log(f"[ERROR] Файл '{file_path}' пуст")
|
||||
self.send_error(client_addr, 0, "Файл пуст")
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
file_basename = os.path.basename(file_path)
|
||||
|
||||
# Регистрируем активную передачу
|
||||
with self.lock:
|
||||
self.active_transfers[client_addr] = {
|
||||
'filename': file_basename,
|
||||
'filesize': filesize,
|
||||
'bytes_sent': 0,
|
||||
'start_time': start_time
|
||||
}
|
||||
|
||||
# Создаем новый сокет для передачи данных
|
||||
transfer_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
transfer_socket.settimeout(TIMEOUT)
|
||||
|
||||
with self.lock:
|
||||
self.transfer_sockets.add(transfer_socket)
|
||||
|
||||
self.log(f"[INFO] Начало передачи файла '{file_basename}' клиенту {client_addr}. Размер файла: {filesize} байт.")
|
||||
|
||||
with open(file_path, 'rb') as file:
|
||||
block_number = 1
|
||||
last_successful_block = 0
|
||||
|
||||
while True:
|
||||
# Читаем блок данных
|
||||
data = file.read(BLOCK_SIZE)
|
||||
|
||||
# Формируем и отправляем пакет данных
|
||||
packet = struct.pack('!HH', 3, block_number) + data
|
||||
|
||||
retries = 0
|
||||
while retries < MAX_RETRIES:
|
||||
try:
|
||||
transfer_socket.sendto(packet, client_addr)
|
||||
|
||||
# Ожидаем подтверждение
|
||||
while True:
|
||||
try:
|
||||
ack_data, ack_addr = transfer_socket.recvfrom(4)
|
||||
if ack_addr == client_addr and len(ack_data) >= 4:
|
||||
opcode, ack_block = struct.unpack('!HH', ack_data)
|
||||
if opcode == 4: # ACK
|
||||
if ack_block == block_number:
|
||||
# Успешное подтверждение
|
||||
last_successful_block = block_number
|
||||
bytes_sent = min((block_number * BLOCK_SIZE), filesize)
|
||||
|
||||
# Обновляем информацию о прогрессе
|
||||
with self.lock:
|
||||
if client_addr in self.active_transfers:
|
||||
self.active_transfers[client_addr]['bytes_sent'] = bytes_sent
|
||||
|
||||
# Логируем статус каждую секунду
|
||||
current_time = time.time()
|
||||
if current_time - start_time >= 1.0:
|
||||
bytes_remaining = filesize - bytes_sent
|
||||
elapsed_time = current_time - start_time
|
||||
self.log(f"[STATUS] Клиент: {client_addr} | Файл: {file_basename} | "
|
||||
f"Отправлено: {bytes_sent}/{filesize} байт | "
|
||||
f"Осталось: {bytes_remaining} байт | "
|
||||
f"Время: {elapsed_time:.2f} сек.")
|
||||
|
||||
break
|
||||
elif ack_block < block_number:
|
||||
# Получен старый ACK, игнорируем
|
||||
continue
|
||||
except socket.timeout:
|
||||
break
|
||||
|
||||
if last_successful_block == block_number:
|
||||
break
|
||||
else:
|
||||
retries += 1
|
||||
self.log(f"[WARN] Таймаут ожидания ACK для блока {block_number} от {client_addr}. "
|
||||
f"Попытка {retries + 1}.")
|
||||
except Exception as e:
|
||||
retries += 1
|
||||
self.log(f"[ERROR] Ошибка при передаче блока {block_number}: {str(e)}")
|
||||
|
||||
if retries >= MAX_RETRIES:
|
||||
self.log(f"[ERROR] Превышено максимальное количество попыток для блока {block_number}")
|
||||
return
|
||||
|
||||
block_number += 1
|
||||
|
||||
# Если отправили меньше BLOCK_SIZE байт, это был последний блок
|
||||
if len(data) < BLOCK_SIZE:
|
||||
break
|
||||
|
||||
self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} завершена успешно")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"[ERROR] Ошибка при передаче файла: {str(e)}")
|
||||
finally:
|
||||
# Очищаем информацию о передаче
|
||||
with self.lock:
|
||||
if client_addr in self.active_transfers:
|
||||
del self.active_transfers[client_addr]
|
||||
if transfer_socket in self.transfer_sockets:
|
||||
self.transfer_sockets.remove(transfer_socket)
|
||||
|
||||
if transfer_socket:
|
||||
try:
|
||||
transfer_socket.close()
|
||||
except:
|
||||
pass
|
||||
103
about_window.py
Normal file
103
about_window.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, BOTH, X, BOTTOM, END
|
||||
import webbrowser
|
||||
|
||||
class AboutWindow(tk.Toplevel):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.title("О программе")
|
||||
self.geometry("600x500")
|
||||
self.resizable(False, False)
|
||||
|
||||
# Сохраняем ссылку на родительское окно
|
||||
self.parent = parent
|
||||
|
||||
# Создаем фрейм для содержимого
|
||||
about_frame = ttk.Frame(self, padding="20")
|
||||
about_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Заголовок
|
||||
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=f"Версия {getattr(parent, 'VERSION', '1.0.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: SPRF555@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(
|
||||
self,
|
||||
text="Закрыть",
|
||||
command=self.destroy
|
||||
).pack(side=BOTTOM, pady=10)
|
||||
|
||||
# Центрируем окно
|
||||
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)
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/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)))
|
||||
@@ -1,224 +0,0 @@
|
||||
#!/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()]
|
||||
@@ -1,190 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,148 +0,0 @@
|
||||
#!/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()
|
||||
298
src/core/app.py
298
src/core/app.py
@@ -1,298 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,412 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,211 +0,0 @@
|
||||
#!/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}")
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,247 +0,0 @@
|
||||
#!/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}
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/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)))
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,200 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,335 +0,0 @@
|
||||
#!/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)} активных передач")
|
||||
@@ -1,4 +0,0 @@
|
||||
pyserial>=3.5
|
||||
tftpy>=0.8.0
|
||||
requests>=2.31.0
|
||||
watchdog>=3.0.0
|
||||
@@ -1,250 +0,0 @@
|
||||
#!/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}")
|
||||
@@ -1,227 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,741 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,200 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/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 ""
|
||||
@@ -1,233 +0,0 @@
|
||||
#!/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="Готов к передаче")
|
||||
165
update_checker.py
Normal file
165
update_checker.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import threading
|
||||
import re
|
||||
from packaging import version
|
||||
import xml.etree.ElementTree as ET
|
||||
import html
|
||||
|
||||
class UpdateCheckError(Exception):
|
||||
"""Исключение для ошибок проверки обновлений"""
|
||||
pass
|
||||
|
||||
class ReleaseType:
|
||||
"""Типы релизов"""
|
||||
STABLE = "stable"
|
||||
PRERELEASE = "prerelease"
|
||||
|
||||
class UpdateChecker:
|
||||
"""Класс для проверки обновлений программы"""
|
||||
|
||||
def __init__(self, current_version, repo_url, include_prereleases=False):
|
||||
self.current_version = current_version
|
||||
self.repo_url = repo_url
|
||||
self.include_prereleases = include_prereleases
|
||||
self.rss_url = f"{repo_url}/releases.rss"
|
||||
self.release_info = None
|
||||
|
||||
def _clean_html(self, html_text):
|
||||
"""Очищает HTML-разметку и форматирует текст"""
|
||||
if not html_text:
|
||||
return ""
|
||||
text = re.sub(r'<[^>]+>', '', html_text)
|
||||
text = html.unescape(text)
|
||||
text = re.sub(r'\n\s*\n', '\n\n', text)
|
||||
return '\n'.join(line.strip() for line in text.splitlines()).strip()
|
||||
|
||||
def _parse_release_info(self, item):
|
||||
"""Извлекает информацию о релизе из RSS item"""
|
||||
title = item.find('title').text if item.find('title') is not None else ''
|
||||
link = item.find('link').text if item.find('link') is not None else ''
|
||||
description = item.find('description').text if item.find('description') is not None else ''
|
||||
content = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
|
||||
content_text = content.text if content is not None else ''
|
||||
|
||||
# Извлекаем версию и проверяем тип релиза из тега
|
||||
version_match = re.search(r'/releases/tag/(?:pre-)?v?(\d+\.\d+(?:\.\d+)?)', link)
|
||||
if not version_match:
|
||||
return None
|
||||
|
||||
version_str = version_match.group(1)
|
||||
# Проверяем наличие префикса pre- в теге
|
||||
is_prerelease = 'pre-' in link.lower()
|
||||
|
||||
# Форматируем название релиза
|
||||
formatted_title = title
|
||||
if title == version_str or not title.strip():
|
||||
# Если заголовок пустой или совпадает с версией, создаем стандартное название
|
||||
release_type = "Пре-релиз" if is_prerelease else "Версия"
|
||||
formatted_title = f"{release_type} {version_str}"
|
||||
elif not re.search(version_str, title):
|
||||
# Если версия не указана в заголовке, добавляем её
|
||||
formatted_title = f"{title} ({version_str})"
|
||||
|
||||
# Форматируем описание
|
||||
formatted_description = self._clean_html(content_text or description)
|
||||
if not formatted_description.strip():
|
||||
formatted_description = "Нет описания"
|
||||
|
||||
# Добавляем метку типа релиза в начало описания
|
||||
release_type_label = "[Пре-релиз] " if is_prerelease else ""
|
||||
formatted_description = f"{release_type_label}{formatted_description}"
|
||||
|
||||
return {
|
||||
'title': formatted_title,
|
||||
'link': link,
|
||||
'description': formatted_description,
|
||||
'version': version_str,
|
||||
'type': ReleaseType.PRERELEASE if is_prerelease else ReleaseType.STABLE
|
||||
}
|
||||
|
||||
def check_updates(self, callback=None):
|
||||
"""Проверяет наличие обновлений в асинхронном режиме"""
|
||||
def check_worker():
|
||||
try:
|
||||
logging.info(f"Текущая версия программы: {self.current_version}")
|
||||
logging.info(f"Проверка пре-релизов: {self.include_prereleases}")
|
||||
logging.info(f"Запрос RSS ленты: {self.rss_url}")
|
||||
|
||||
response = requests.get(self.rss_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
root = ET.fromstring(response.content)
|
||||
items = root.findall('.//item')
|
||||
if not items:
|
||||
raise UpdateCheckError("Релизы не найдены")
|
||||
|
||||
logging.info(f"Найдено {len(items)} релизов")
|
||||
|
||||
latest_version = None
|
||||
latest_info = None
|
||||
|
||||
for item in items:
|
||||
release_info = self._parse_release_info(item)
|
||||
if not release_info:
|
||||
continue
|
||||
|
||||
is_prerelease = release_info['type'] == ReleaseType.PRERELEASE
|
||||
logging.info(
|
||||
f"Проверка релиза: {release_info['title']}, "
|
||||
f"версия: {release_info['version']}, "
|
||||
f"тип: {'пре-релиз' if is_prerelease else 'стабильная'}"
|
||||
)
|
||||
|
||||
# Пропускаем пре-релизы если они не включены
|
||||
if is_prerelease and not self.include_prereleases:
|
||||
logging.info(f"Пропуск пре-релиза: {release_info['version']}")
|
||||
continue
|
||||
|
||||
# Сравниваем версии
|
||||
try:
|
||||
current_ver = version.parse(latest_version or "0.0.0")
|
||||
new_ver = version.parse(release_info['version'].split('-')[0]) # Убираем суффикс для сравнения
|
||||
|
||||
if new_ver > current_ver:
|
||||
latest_version = release_info['version']
|
||||
latest_info = release_info
|
||||
logging.info(f"Новая версия: {latest_version}")
|
||||
|
||||
except version.InvalidVersion as e:
|
||||
logging.warning(f"Некорректный формат версии {release_info['version']}: {e}")
|
||||
continue
|
||||
|
||||
if not latest_info:
|
||||
raise UpdateCheckError("Не найдены подходящие версии")
|
||||
|
||||
self.release_info = latest_info
|
||||
|
||||
# Сравниваем с текущей версией
|
||||
current_ver = version.parse(self.current_version)
|
||||
latest_ver = version.parse(latest_version.split('-')[0])
|
||||
update_available = latest_ver > current_ver
|
||||
|
||||
logging.info(f"Сравнение версий: текущая {current_ver} <-> последняя {latest_ver}")
|
||||
logging.info(f"Доступно обновление: {update_available}")
|
||||
|
||||
if callback:
|
||||
callback(update_available, None)
|
||||
|
||||
except UpdateCheckError as e:
|
||||
logging.error(str(e))
|
||||
if callback:
|
||||
callback(False, str(e))
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при проверке обновлений: {e}", exc_info=True)
|
||||
if callback:
|
||||
callback(False, str(e))
|
||||
|
||||
threading.Thread(target=check_worker, daemon=True).start()
|
||||
|
||||
def get_release_notes(self):
|
||||
"""Возвращает информацию о последнем релизе"""
|
||||
return self.release_info
|
||||
Reference in New Issue
Block a user