Merge pull request 'TFTP' (#3) from TFTP into main

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2025-02-16 00:39:43 +00:00
3 changed files with 664 additions and 20 deletions

View File

@@ -37,13 +37,15 @@ import serial
import serial.tools.list_ports
from serial.serialutil import SerialException
from about_window import AboutWindow
from TFTPServer import TFTPServer
# from TFTPServer import TFTPServerThread
import socket
# Создаем необходимые папки
os.makedirs("Logs", exist_ok=True)
os.makedirs("Configs", exist_ok=True)
os.makedirs("Settings", exist_ok=True)
# os.makedirs("Firmware", exist_ok=True)
os.makedirs("Firmware", exist_ok=True)
# Файл настроек находится в папке Settings
SETTINGS_FILE = os.path.join("Settings", "settings.json")
@@ -137,25 +139,37 @@ def list_serial_ports():
# Функции работы с сетевыми адаптерами (не используются)
# ==========================
# def list_network_adapters():
# """Возвращает список названий сетевых адаптеров (Windows)."""
# adapters = []
# if platform.system() == "Windows":
# try:
# output = subprocess.check_output(
# 'wmic nic get NetConnectionID',
# shell=True,
# encoding="cp866"
# )
# for line in output.splitlines():
# line = line.strip()
# if line and line != "NetConnectionID":
# adapters.append(line)
# except Exception as e:
# logging.error(f"Ошибка при получении списка адаптеров: {e}", exc_info=True)
# else:
# adapters = ["eth0"]
# return adapters
def get_network_adapters():
"""Получение списка сетевых адаптеров и их IP-адресов."""
adapters = []
try:
# Получаем имя хоста
hostname = socket.gethostname()
# Получаем все адреса для данного хоста
addresses = socket.getaddrinfo(hostname, None)
# Создаем множество для хранения уникальных IP-адресов
unique_ips = set()
for addr in addresses:
ip = addr[4][0]
# Пропускаем IPv6 и локальные адреса
if ':' not in ip and not ip.startswith('127.'):
unique_ips.add(ip)
# Добавляем все найденные IP-адреса в список
for ip in sorted(unique_ips):
adapters.append(f"{ip}")
# Добавляем 0.0.0.0 для прослушивания всех интерфейсов
adapters.insert(0, "0.0.0.0")
except Exception as e:
logging.error(f"Ошибка при получении списка сетевых адаптеров: {e}", exc_info=True)
# В случае ошибки возвращаем хотя бы 0.0.0.0
adapters = ["0.0.0.0"]
return adapters
# ==========================
# Функции работы с COM-соединением
@@ -535,6 +549,7 @@ class SerialAppGUI(tk.Tk):
self.option_add("*Font", default_font)
self.settings = settings
self.connection = None
self.tftp_server = None
# Глобальные биндинги
self.bind_class("Text", "<Control-c>", lambda event: event.widget.event_generate("<<Copy>>"))
@@ -613,14 +628,17 @@ class SerialAppGUI(tk.Tk):
interactive_frame = ttk.Frame(self.notebook)
file_exec_frame = ttk.Frame(self.notebook)
config_editor_frame = ttk.Frame(self.notebook)
tftp_frame = ttk.Frame(self.notebook)
self.notebook.add(interactive_frame, text="Интерактивный режим")
self.notebook.add(file_exec_frame, text="Выполнение файла")
self.notebook.add(config_editor_frame, text="Редактор конфигурации")
self.notebook.add(tftp_frame, text="TFTP Сервер")
self.create_interactive_tab(interactive_frame)
self.create_file_exec_tab(file_exec_frame)
self.create_config_editor_tab(config_editor_frame)
self.create_tftp_tab(tftp_frame)
# -------------- Вкладка "Интерактивный режим" --------------
def create_interactive_tab(self, frame):
@@ -823,6 +841,259 @@ class SerialAppGUI(tk.Tk):
about_window.transient(self)
about_window.grab_set()
def create_tftp_tab(self, frame):
"""Создание вкладки TFTP сервера."""
# Создаем фрейм для управления TFTP сервером
tftp_frame = ttk.Frame(frame)
tftp_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Создаем и размещаем элементы управления
controls_frame = ttk.LabelFrame(tftp_frame, text="Управление TFTP сервером")
controls_frame.pack(fill=X, padx=5, pady=5)
# IP адрес
ip_frame = ttk.Frame(controls_frame)
ip_frame.pack(fill=X, padx=5, pady=2)
ttk.Label(ip_frame, text="IP адрес:").pack(side=LEFT, padx=5)
self.tftp_ip_var = StringVar(value="0.0.0.0")
self.tftp_ip_combo = ttk.Combobox(ip_frame, textvariable=self.tftp_ip_var, state="readonly")
self.tftp_ip_combo.pack(side=LEFT, fill=X, expand=True, padx=5)
ttk.Button(ip_frame, text="Обновить", command=self.update_network_adapters).pack(side=LEFT, padx=5)
# Заполняем список адаптеров
self.update_network_adapters()
# Порт
port_frame = ttk.Frame(controls_frame)
port_frame.pack(fill=X, padx=5, pady=2)
ttk.Label(port_frame, text="Порт:").pack(side=LEFT, padx=5)
self.tftp_port_var = StringVar(value="69")
self.tftp_port_entry = ttk.Entry(port_frame, textvariable=self.tftp_port_var)
self.tftp_port_entry.pack(fill=X, expand=True, padx=5)
# Кнопки управления
buttons_frame = ttk.Frame(controls_frame)
buttons_frame.pack(fill=X, padx=5, pady=5)
self.start_tftp_button = ttk.Button(
buttons_frame,
text="Запустить сервер",
command=self.start_tftp_server
)
self.start_tftp_button.pack(side=LEFT, padx=5)
self.stop_tftp_button = ttk.Button(
buttons_frame,
text="Остановить сервер",
command=self.stop_tftp_server,
state="disabled"
)
self.stop_tftp_button.pack(side=LEFT, padx=5)
# Лог сервера
log_frame = ttk.LabelFrame(tftp_frame, text="Лог сервера")
log_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
self.tftp_log_text = tk.Text(log_frame, wrap=tk.WORD, height=10)
self.tftp_log_text.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Добавляем скроллбар для лога
scrollbar = ttk.Scrollbar(self.tftp_log_text, command=self.tftp_log_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tftp_log_text.config(yscrollcommand=scrollbar.set)
# Статус передач
transfers_frame = ttk.LabelFrame(tftp_frame, text="Активные передачи")
transfers_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Создаем таблицу для отображения активных передач
columns = ("client", "filename", "progress", "remaining", "time")
self.transfers_tree = ttk.Treeview(transfers_frame, columns=columns, show="headings")
# Настраиваем заголовки колонок
self.transfers_tree.heading("client", text="Клиент")
self.transfers_tree.heading("filename", text="Файл")
self.transfers_tree.heading("progress", text="Прогресс")
self.transfers_tree.heading("remaining", text="Осталось")
self.transfers_tree.heading("time", text="Время")
# Настраиваем ширину колонок
self.transfers_tree.column("client", width=120)
self.transfers_tree.column("filename", width=150)
self.transfers_tree.column("progress", width=100)
self.transfers_tree.column("remaining", width=100)
self.transfers_tree.column("time", width=80)
self.transfers_tree.pack(fill=BOTH, expand=True, padx=5, pady=5)
# Инициализация TFTP сервера
self.tftp_server = None
self.tftp_server_thread = None
def start_tftp_server(self):
"""Запуск TFTP сервера."""
try:
# Получаем выбранный IP-адрес
ip = self.tftp_ip_var.get()
if not ip:
messagebox.showerror("Ошибка", "Выберите IP-адрес для TFTP сервера")
return
# Проверяем корректность порта
try:
port = int(self.tftp_port_var.get())
if port <= 0 or port > 65535:
raise ValueError("Порт должен быть в диапазоне 1-65535")
except ValueError as e:
messagebox.showerror("Ошибка", f"Некорректный порт: {str(e)}")
return
# Создаем экземпляр TFTP сервера
self.tftp_server = TFTPServer("Firmware")
# Устанавливаем callback для логирования
def log_callback(message):
# Фильтруем дублирующиеся сообщения о запуске/остановке сервера
if "[INFO] TFTP сервер запущен" in message and hasattr(self, '_server_started'):
return
if "[INFO] TFTP сервер остановлен" in message and hasattr(self, '_server_stopped'):
return
self.append_tftp_log(message)
# Устанавливаем флаги для отслеживания состояния
if "[INFO] TFTP сервер запущен" in message:
self._server_started = True
elif "[INFO] TFTP сервер остановлен" in message:
self._server_stopped = True
# Обновляем информацию о передачах
self.update_transfers_info()
self.tftp_server.set_log_callback(log_callback)
# Запускаем сервер в отдельном потоке
self.tftp_server_thread = threading.Thread(
target=self.run_tftp_server,
args=(ip, port),
daemon=True
)
self.tftp_server_thread.start()
# Обновляем состояние кнопок и элементов управления
self.start_tftp_button.config(state="disabled")
self.stop_tftp_button.config(state="normal")
self.tftp_ip_combo.config(state="disabled")
self.tftp_port_entry.config(state="disabled")
# Запускаем периодическое обновление информации о передачах
self.update_transfers_periodically()
except Exception as e:
self.append_tftp_log(f"[ERROR] Ошибка запуска сервера: {str(e)}")
messagebox.showerror("Ошибка", f"Не удалось запустить TFTP сервер: {str(e)}")
def run_tftp_server(self, ip, port):
"""Запуск TFTP сервера в отдельном потоке."""
try:
self.tftp_server.start_server(ip, port)
except Exception as e:
self.append_tftp_log(f"[ERROR] Ошибка работы сервера: {str(e)}")
def stop_tftp_server(self):
"""Остановка TFTP сервера."""
if self.tftp_server:
try:
# Отключаем кнопки на время остановки сервера
self.start_tftp_button.config(state="disabled")
self.stop_tftp_button.config(state="disabled")
# Сбрасываем флаги состояния
if hasattr(self, '_server_started'):
delattr(self, '_server_started')
if hasattr(self, '_server_stopped'):
delattr(self, '_server_stopped')
# Останавливаем сервер
self.tftp_server.stop_server()
# Ждем завершения потока сервера с таймаутом
if self.tftp_server_thread:
self.tftp_server_thread.join(timeout=5.0)
if self.tftp_server_thread.is_alive():
self.append_tftp_log("[WARN] Превышено время ожидания остановки сервера")
# Очищаем ссылки на сервер и поток
self.tftp_server = None
self.tftp_server_thread = None
# Обновляем состояние кнопок
self.start_tftp_button.config(state="normal")
self.stop_tftp_button.config(state="disabled")
self.tftp_ip_combo.config(state="normal")
self.tftp_port_entry.config(state="normal")
# Очищаем таблицу передач
for item in self.transfers_tree.get_children():
self.transfers_tree.delete(item)
except Exception as e:
self.append_tftp_log(f"[ERROR] Ошибка остановки сервера: {str(e)}")
messagebox.showerror("Ошибка", f"Не удалось остановить TFTP сервер: {str(e)}")
# Восстанавливаем состояние кнопок в случае ошибки
self.start_tftp_button.config(state="disabled")
self.stop_tftp_button.config(state="normal")
def append_tftp_log(self, message):
"""Добавление сообщения в лог TFTP сервера."""
self.tftp_log_text.insert(END, message + "\n")
self.tftp_log_text.see(END)
def update_transfers_info(self):
"""Обновление информации об активных передачах."""
if not self.tftp_server:
return
# Очищаем текущие записи
for item in self.transfers_tree.get_children():
self.transfers_tree.delete(item)
# Добавляем информацию о текущих передачах
for client_addr, transfer_info in self.tftp_server.active_transfers.items():
filename = transfer_info['filename']
bytes_sent = transfer_info['bytes_sent']
filesize = transfer_info['filesize']
start_time = transfer_info['start_time']
# Вычисляем прогресс
progress = f"{bytes_sent}/{filesize} байт"
remaining = filesize - bytes_sent
elapsed_time = time.time() - start_time
# Добавляем запись в таблицу
self.transfers_tree.insert("", END, values=(
f"{client_addr[0]}:{client_addr[1]}",
filename,
progress,
f"{remaining} байт",
f"{elapsed_time:.1f}с"
))
def update_transfers_periodically(self):
"""Периодическое обновление информации о передачах."""
if self.tftp_server and self.tftp_server.running:
self.update_transfers_info()
# Планируем следующее обновление через 1 секунду
self.after(1000, self.update_transfers_periodically)
def update_network_adapters(self):
"""Обновление списка сетевых адаптеров."""
adapters = get_network_adapters()
self.tftp_ip_combo["values"] = adapters
if not self.tftp_ip_var.get() in adapters:
self.tftp_ip_var.set(adapters[0])
# ==========================
# Парсер аргументов (не используется)
# ==========================

372
TFTPServer.py Normal file
View File

@@ -0,0 +1,372 @@
#!/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
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
}
self.log(f"[INFO] Начало передачи файла '{file_basename}' клиенту {client_addr}. Размер файла: {filesize} байт.")
# Создаем отдельный сокет для передачи файла
transfer_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
transfer_socket.settimeout(5.0) # Таймаут ожидания ACK
# Добавляем сокет в множество активных сокетов
with self.lock:
self.transfer_sockets.add(transfer_socket)
block_num = 1
bytes_sent = 0
last_progress_time = time.time()
try:
with open(file_path, "rb") as f:
while self.running: # Проверяем флаг running
try:
data_block = f.read(BLOCK_SIZE)
if not data_block: # Достигнут конец файла
break
# Проверяем флаг running перед отправкой блока
if not self.running:
raise Exception("Передача прервана: сервер остановлен")
# Формируем TFTP пакет данных
packet = struct.pack("!HH", 3, block_num) + data_block
attempts = 0
ack_received = False
# Попытка отправки текущего блока (до 3 повторных попыток)
while attempts < 3 and not ack_received and self.running:
if transfer_socket is None:
raise Exception("Сокет передачи закрыт")
try:
transfer_socket.sendto(packet, client_addr)
# Логируем прогресс каждую секунду
current_time = time.time()
if current_time - last_progress_time >= 1.0:
elapsed_time = current_time - start_time
remaining = filesize - bytes_sent
self.log(f"[STATUS] Клиент: {client_addr} | Файл: {file_basename} | "
f"Отправлено: {bytes_sent}/{filesize} байт | "
f"Осталось: {remaining} байт | "
f"Время: {elapsed_time:.2f} сек.")
last_progress_time = current_time
# Ожидаем подтверждение
ack_data, addr = transfer_socket.recvfrom(4)
if addr == client_addr:
ack_opcode, ack_block = struct.unpack("!HH", ack_data)
if ack_opcode == 4 and ack_block == block_num:
ack_received = True
bytes_sent += len(data_block)
with self.lock:
if client_addr in self.active_transfers:
self.active_transfers[client_addr]['bytes_sent'] = bytes_sent
else:
self.log(f"[WARN] Неверный ACK от {client_addr}. "
f"Ожидался блок {block_num}, получен {ack_block}.")
except socket.timeout:
attempts += 1
self.log(f"[WARN] Таймаут ожидания ACK для блока {block_num} "
f"от {client_addr}. Попытка {attempts+1}.")
except socket.error as e:
if not self.running:
raise Exception("Передача прервана: сервер остановлен")
self.log(f"[ERROR] Ошибка сокета при отправке блока {block_num}: {str(e)}")
attempts += 1
except Exception as e:
if not self.running:
raise Exception("Передача прервана: сервер остановлен")
self.log(f"[ERROR] Ошибка при отправке блока {block_num}: {str(e)}")
attempts += 1
if not ack_received:
raise Exception(f"Не удалось получить подтверждение для блока {block_num}")
block_num = (block_num + 1) % 65536
except Exception as e:
if not self.running:
raise Exception("Передача прервана: сервер остановлен")
raise
if bytes_sent == filesize:
elapsed_time = time.time() - start_time
self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} "
f"завершена за {elapsed_time:.2f} сек. Всего отправлено {bytes_sent} байт.")
except Exception as e:
if not self.running:
self.log(f"[INFO] Передача файла '{file_basename}' клиенту {client_addr} прервана: сервер остановлен")
raise
except Exception as e:
if not self.running:
return # Не логируем повторно о прерывании передачи
self.log(f"[ERROR] Ошибка при передаче файла '{os.path.basename(file_path)}' "
f"клиенту {client_addr}: {str(e)}")
try:
self.send_error(client_addr, 0, str(e))
except:
pass
finally:
# Закрываем сокет передачи
if transfer_socket:
try:
with self.lock:
self.transfer_sockets.discard(transfer_socket)
transfer_socket.close()
transfer_socket = None
except:
pass
# Удаляем информацию о передаче
with self.lock:
if client_addr in self.active_transfers:
del self.active_transfers[client_addr]

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
tftpy>=0.8.0