From 029371fa680dfca8501582b3f2050b46afe49d07 Mon Sep 17 00:00:00 2001 From: Lowa Date: Fri, 14 Feb 2025 02:47:39 +0300 Subject: [PATCH] Enhance error handling and retry mechanism for command execution - Add a new `send_login_password()` function to centralize login/password input logic - Implement retry mechanism for both line and block command modes - Add support for detecting and handling command errors (marked by '^' symbol) - Limit retry attempts to 3 times for each command - Improve logging and error reporting for command execution - Update interactive command processing in SerialAppGUI to include retry logic --- ComConfigCopy.py | 307 +++++++++++++++++++++++++---------------------- TFTPServer.py | 98 +++++++++++++++ 2 files changed, 261 insertions(+), 144 deletions(-) create mode 100644 TFTPServer.py diff --git a/ComConfigCopy.py b/ComConfigCopy.py index c560e08..96cacc8 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -43,6 +43,7 @@ from tkinter import ttk import serial import serial.tools.list_ports from serial.serialutil import SerialException +from TFTPServer import TFTPServerThread # Создаем необходимые папки os.makedirs("Logs", exist_ok=True) @@ -158,6 +159,31 @@ def create_connection(settings): logging.error(f"Неизвестная ошибка подключения: {e}", exc_info=True) return None + + +# Проверка наличия логина и пароля в настройках и отправка их на устройство + +def send_login_password(serial_connection, login=None, password=None, is_gui=False): + """Отправка логина и пароля на устройство.""" + if not login: + if is_gui: + login = simpledialog.askstring("Login", "Введите логин:") + if login is None: + login = "" + else: + login = input("Введите логин: ") + + if not password: + if is_gui: + password = simpledialog.askstring("Password", "Введите пароль:", show="*") + if password is None: + password = "" + else: + password = getpass("Введите пароль: ") + + +# Чтение ответа от устройства с учётом таймаута. + def read_response(serial_connection, timeout, login=None, password=None, is_gui=False): """ Чтение ответа от устройства с учётом таймаута. @@ -184,28 +210,12 @@ def read_response(serial_connection, timeout, login=None, password=None, is_gui= last_line = lines[-1].strip() if re.search(r'(login:|username:)$', last_line, re.IGNORECASE): - if not login: - if is_gui: - login = simpledialog.askstring("Login", "Введите логин:") - if login is None: - login = "" - else: - login = input("Введите логин: ") - serial_connection.write((login + "\n").encode()) - logging.info("Отправлен логин.") + send_login_password(serial_connection, login, None, is_gui) response = b"" continue if re.search(r'(password:)$', last_line, re.IGNORECASE): - if not password: - if is_gui: - password = simpledialog.askstring("Password", "Введите пароль:", show="*") - if password is None: - password = "" - else: - password = getpass("Введите пароль: ") - serial_connection.write((password + "\n").encode()) - logging.info("Отправлен пароль.") + send_login_password(serial_connection, None, password, is_gui) response = b"" continue @@ -256,6 +266,8 @@ def execute_commands_from_file( """ Выполнение команд из файла конфигурации. Если передан log_callback, вывод будет отображаться в GUI. + Теперь при обнаружении ошибки (например, если в ответе присутствует символ '^') + команда будет отправляться повторно. """ try: with open(filename, "r", encoding="utf-8") as file: @@ -264,26 +276,51 @@ def execute_commands_from_file( logging.info(msg) if log_callback: log_callback(msg) + # Если выбран построчный режим if copy_mode == "line": for cmd in lines: cmd = cmd.strip() - msg = f"\nОтправка команды: {cmd}\n" - if log_callback: - log_callback(msg) - serial_connection.write((cmd + "\n").encode()) - logging.info(f"Отправлена команда: {cmd}") - response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui) - if response: - msg = f"Ответ устройства:\n{response}\n" + max_attempts = 3 + attempt = 0 + while attempt < max_attempts: + msg = f"\nОтправка команды: {cmd} (Попытка {attempt+1} из {max_attempts})\n" if log_callback: log_callback(msg) - logging.info(f"Ответ устройства:\n{response}") - else: - msg = f"Ответ не получен для команды: {cmd}\n" + serial_connection.write((cmd + "\n").encode()) + logging.info(f"Отправлена команда: {cmd} (Попытка {attempt+1})") + response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui) + if response: + if '^' in response: + msg = ( + f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n" + f"Ответ устройства:\n{response}\n" + f"Повторная отправка команды...\n" + ) + if log_callback: + log_callback(msg) + logging.warning(f"Ошибка в команде: {cmd}. Повторная отправка.") + attempt += 1 + time.sleep(1) + continue + else: + msg = f"Ответ устройства:\n{response}\n" + if log_callback: + log_callback(msg) + logging.info(f"Ответ устройства:\n{response}") + break + else: + msg = f"Ответ не получен для команды: {cmd}\n" + if log_callback: + log_callback(msg) + logging.warning(f"Нет ответа для команды: {cmd}") + break + if attempt == max_attempts: + msg = f"[ERROR] Команда не выполнена корректно после {max_attempts} попыток: {cmd}\n" if log_callback: log_callback(msg) - logging.warning(f"Нет ответа для команды: {cmd}") + logging.error(msg) time.sleep(1) + # Если выбран блочный режим elif copy_mode == "block": blocks = generate_command_blocks(lines, block_size) for block in blocks: @@ -293,16 +330,71 @@ def execute_commands_from_file( serial_connection.write((block + "\n").encode()) logging.info(f"Отправлен блок команд:\n{block}") response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui) - if response: - msg = f"Ответ устройства:\n{response}\n" + # Если обнаружена ошибка в ответе на блок, отправляем команды по очереди + if response and '^' in response: + msg = ( + f"[WARNING] Обнаружена ошибка при выполнении блока команд.\n" + f"Ответ устройства:\n{response}\n" + f"Пересылаются команды по отдельности...\n" + ) if log_callback: log_callback(msg) - logging.info(f"Ответ устройства:\n{response}") + logging.warning("Ошибка в блочном режиме – отправляем команды индивидуально.") + for line in block.splitlines(): + cmd = line.strip() + if not cmd: + continue + max_attempts = 3 + attempt = 0 + while attempt < max_attempts: + sub_msg = f"\nОтправка команды: {cmd} (Попытка {attempt+1} из {max_attempts})\n" + if log_callback: + log_callback(sub_msg) + serial_connection.write((cmd + "\n").encode()) + logging.info(f"Отправлена команда: {cmd} (Попытка {attempt+1})") + sub_response = read_response(serial_connection, timeout, login=login, password=password, is_gui=is_gui) + if sub_response: + if '^' in sub_response: + sub_msg = ( + f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n" + f"Ответ устройства:\n{sub_response}\n" + f"Повторная отправка команды...\n" + ) + if log_callback: + log_callback(sub_msg) + logging.warning(f"Ошибка в команде: {cmd}. Повторная отправка.") + attempt += 1 + time.sleep(1) + continue + else: + sub_msg = f"Ответ устройства:\n{sub_response}\n" + if log_callback: + log_callback(sub_msg) + logging.info(f"Ответ устройства:\n{sub_response}") + break + else: + sub_msg = f"Ответ не получен для команды: {cmd}\n" + if log_callback: + log_callback(sub_msg) + logging.warning(f"Нет ответа для команды: {cmd}") + break + if attempt == max_attempts: + sub_msg = f"[ERROR] Команда не выполнена корректно после {max_attempts} попыток: {cmd}\n" + if log_callback: + log_callback(sub_msg) + logging.error(sub_msg) + time.sleep(1) else: - msg = f"Ответ не получен для блока:\n{block}\n" - if log_callback: - log_callback(msg) - logging.warning(f"Нет ответа для блока:\n{block}") + if response: + msg = f"Ответ устройства:\n{response}\n" + if log_callback: + log_callback(msg) + logging.info(f"Ответ устройства:\n{response}") + else: + msg = f"Ответ не получен для блока:\n{block}\n" + if log_callback: + log_callback(msg) + logging.warning(f"Нет ответа для блока:\n{block}") time.sleep(1) except SerialException as e: msg = f"Ошибка при выполнении команды: {e}\n" @@ -315,98 +407,6 @@ def execute_commands_from_file( log_callback(msg) logging.error(f"Ошибка при выполнении команд из файла: {e}", exc_info=True) -# ========================== -# Реализация TFTP-сервера для раздачи папки Firmware -# ========================== - -class TFTPServerThread(threading.Thread): - def __init__(self, host, port, log_callback): - super().__init__() - self.host = host - self.port = port - self.log_callback = log_callback - self.running = True - self.sock = None - - def run(self): - try: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.sock.bind((self.host, self.port)) - self.log_callback(f"TFTP сервер запущен на {self.host}:{self.port}\n") - except Exception as e: - self.log_callback(f"Ошибка запуска TFTP сервера: {e}\n") - return - - while self.running: - try: - self.sock.settimeout(1) - data, addr = self.sock.recvfrom(1024) - if data: - self.handle_rrq(data, addr) - except socket.timeout: - continue - except Exception as e: - self.log_callback(f"Ошибка: {e}\n") - if self.sock: - self.sock.close() - self.log_callback("TFTP сервер остановлен.\n") - - def handle_rrq(self, data, addr): - opcode = int.from_bytes(data[0:2], byteorder='big') - if opcode != 1: - self.log_callback(f"Получен не RRQ запрос от {addr}\n") - return - parts = data[2:].split(b'\0') - if len(parts) < 2: - self.log_callback(f"Неверный формат RRQ от {addr}\n") - return - req_filename = parts[0].decode('utf-8', errors='ignore') - mode = parts[1].decode('utf-8', errors='ignore') - self.log_callback(f"Получен RRQ для файла '{req_filename}' (режим {mode}) от {addr}\n") - filepath = os.path.join("Firmware", req_filename) - if not os.path.exists(filepath): - self.log_callback(f"Файл '{req_filename}' не найден в папке Firmware\n") - return - try: - filesize = os.path.getsize(filepath) - f = open(filepath, 'rb') - except Exception as e: - self.log_callback(f"Ошибка открытия файла '{req_filename}': {e}\n") - return - block_num = 1 - bytes_sent = 0 - start_time = time.time() - while True: - block_data = f.read(512) - data_packet = b'\x00\x03' + block_num.to_bytes(2, byteorder='big') + block_data - self.sock.sendto(data_packet, addr) - self.log_callback(f"Отправлен блок {block_num} ({len(block_data)} байт)\n") - try: - self.sock.settimeout(5) - ack, ack_addr = self.sock.recvfrom(1024) - if ack_addr != addr: - continue - ack_opcode = int.from_bytes(ack[0:2], byteorder='big') - ack_block = int.from_bytes(ack[2:4], byteorder='big') - if ack_opcode != 4 or ack_block != block_num: - self.log_callback(f"Неверный ACK от {addr}\n") - break - except socket.timeout: - self.log_callback("Таймаут ожидания ACK\n") - break - bytes_sent += len(block_data) - elapsed = time.time() - start_time - speed = bytes_sent / elapsed if elapsed > 0 else 0 - progress = (bytes_sent / filesize) * 100 if filesize > 0 else 0 - self.log_callback(f"Прогресс: {progress:.2f}% | Скорость: {speed:.2f} байт/с\n") - if len(block_data) < 512: - break - block_num += 1 - f.close() - - def stop(self): - self.running = False - # ========================== # Графический интерфейс (Tkinter) с улучшенной визуализацией # ========================== @@ -608,21 +608,40 @@ class SerialAppGUI(tk.Tk): def process_command(self, cmd): try: - self.connection.write((cmd + "\n").encode()) - logging.info(f"Отправлена команда: {cmd}") - response = read_response( - self.connection, - self.settings.get("timeout", 10), - login=self.settings.get("login"), - password=self.settings.get("password"), - is_gui=True, - ) - if response: - self.append_interactive_text(f"[INFO] Ответ устройства:\n{response}\n") - logging.info(f"Получен ответ:\n{response}") - else: - self.append_interactive_text("[WARN] Ответ не получен.\n") - logging.warning("Нет ответа от устройства в течение таймаута.") + max_attempts = 3 + attempt = 0 + while attempt < max_attempts: + self.connection.write((cmd + "\n").encode()) + logging.info(f"Отправлена команда: {cmd} (Попытка {attempt+1})") + response = read_response( + self.connection, + self.settings.get("timeout", 10), + login=self.settings.get("login"), + password=self.settings.get("password"), + is_gui=True, + ) + if response: + if '^' in response: + self.append_interactive_text( + f"[ERROR] Обнаружена ошибка при выполнении команды: {cmd}\n" + f"Ответ устройства:\n{response}\n" + f"Повторная отправка команды...\n" + ) + logging.warning(f"Ошибка в команде: {cmd}. Попытка повторной отправки.") + attempt += 1 + time.sleep(1) + continue + else: + self.append_interactive_text(f"[INFO] Ответ устройства:\n{response}\n") + logging.info(f"Получен ответ:\n{response}") + break + else: + self.append_interactive_text("[WARN] Ответ не получен.\n") + logging.warning("Нет ответа от устройства в течение таймаута.") + break + if attempt == max_attempts: + self.append_interactive_text(f"[ERROR] Команда не выполнена корректно после {max_attempts} попыток: {cmd}\n") + logging.error(f"Команда не выполнена корректно после {max_attempts} попыток: {cmd}") except SerialException as e: self.append_interactive_text(f"[ERROR] Ошибка при отправке команды: {e}\n") logging.error(f"Ошибка отправки команды: {e}", exc_info=True) diff --git a/TFTPServer.py b/TFTPServer.py new file mode 100644 index 0000000..627dfcd --- /dev/null +++ b/TFTPServer.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Модуль для TFTP-сервера. +""" + +import socket +import threading +import os +import time + +class TFTPServerThread(threading.Thread): + def __init__(self, host, port, log_callback): + super().__init__() + self.host = host + self.port = port + self.log_callback = log_callback + self.running = True + self.sock = None + + def run(self): + try: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.bind((self.host, self.port)) + self.log_callback(f"TFTP сервер запущен на {self.host}:{self.port}\n") + except Exception as e: + self.log_callback(f"Ошибка запуска TFTP сервера: {e}\n") + return + + while self.running: + try: + self.sock.settimeout(1) + data, addr = self.sock.recvfrom(1024) + if data: + self.handle_rrq(data, addr) + except socket.timeout: + continue + except Exception as e: + self.log_callback(f"Ошибка: {e}\n") + if self.sock: + self.sock.close() + self.log_callback("TFTP сервер остановлен.\n") + + def handle_rrq(self, data, addr): + opcode = int.from_bytes(data[0:2], byteorder='big') + if opcode != 1: + self.log_callback(f"Получен не RRQ запрос от {addr}\n") + return + parts = data[2:].split(b'\0') + if len(parts) < 2: + self.log_callback(f"Неверный формат RRQ от {addr}\n") + return + req_filename = parts[0].decode('utf-8', errors='ignore') + mode = parts[1].decode('utf-8', errors='ignore') + self.log_callback(f"Получен RRQ для файла '{req_filename}' (режим {mode}) от {addr}\n") + filepath = os.path.join("Firmware", req_filename) + if not os.path.exists(filepath): + self.log_callback(f"Файл '{req_filename}' не найден в папке Firmware\n") + return + try: + filesize = os.path.getsize(filepath) + f = open(filepath, 'rb') + except Exception as e: + self.log_callback(f"Ошибка открытия файла '{req_filename}': {e}\n") + return + block_num = 1 + bytes_sent = 0 + start_time = time.time() + while True: + block_data = f.read(512) + data_packet = b'\x00\x03' + block_num.to_bytes(2, byteorder='big') + block_data + self.sock.sendto(data_packet, addr) + self.log_callback(f"Отправлен блок {block_num} ({len(block_data)} байт)\n") + try: + self.sock.settimeout(5) + ack, ack_addr = self.sock.recvfrom(1024) + if ack_addr != addr: + continue + ack_opcode = int.from_bytes(ack[0:2], byteorder='big') + ack_block = int.from_bytes(ack[2:4], byteorder='big') + if ack_opcode != 4 or ack_block != block_num: + self.log_callback(f"Неверный ACK от {addr}\n") + break + except socket.timeout: + self.log_callback("Таймаут ожидания ACK\n") + break + bytes_sent += len(block_data) + elapsed = time.time() - start_time + speed = bytes_sent / elapsed if elapsed > 0 else 0 + progress = (bytes_sent / filesize) * 100 if filesize > 0 else 0 + self.log_callback(f"Прогресс: {progress:.2f}% | Скорость: {speed:.2f} байт/с\n") + if len(block_data) < 512: + break + block_num += 1 + f.close() + + def stop(self): + self.running = False