Add update checking and documentation features

- Implement UpdateChecker for version comparison and update notifications
- Add menu options for documentation and update checking
- Enhance AboutWindow with dynamic version display
- Update requirements.txt with new dependencies
- Create infrastructure for opening local documentation
- Improve application menu with additional help options
This commit is contained in:
2025-02-16 04:50:33 +03:00
parent 2e2dd9e705
commit d1a870fed7
5 changed files with 333 additions and 35 deletions

View File

@@ -17,6 +17,7 @@ import re
import sys import sys
import threading import threading
import time import time
import webbrowser
from getpass import getpass from getpass import getpass
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import tkinter as tk import tkinter as tk
@@ -40,12 +41,17 @@ from about_window import AboutWindow
from TFTPServer import TFTPServer from TFTPServer import TFTPServer
# from TFTPServer import TFTPServerThread # from TFTPServer import TFTPServerThread
import socket import socket
from update_checker import UpdateChecker
# Версия программы
VERSION = "1.1.0"
# Создаем необходимые папки # Создаем необходимые папки
os.makedirs("Logs", exist_ok=True) os.makedirs("Logs", exist_ok=True)
os.makedirs("Configs", exist_ok=True) os.makedirs("Configs", exist_ok=True)
os.makedirs("Settings", exist_ok=True) os.makedirs("Settings", exist_ok=True)
os.makedirs("Firmware", exist_ok=True) os.makedirs("Firmware", exist_ok=True)
os.makedirs("docs", exist_ok=True)
# Файл настроек находится в папке Settings # Файл настроек находится в папке Settings
SETTINGS_FILE = os.path.join("Settings", "settings.json") SETTINGS_FILE = os.path.join("Settings", "settings.json")
@@ -643,10 +649,22 @@ class SerialAppGUI(tk.Tk):
super().__init__() super().__init__()
self.title("ComConfigCopy") self.title("ComConfigCopy")
self.geometry("900x700") self.geometry("900x700")
# Добавляем VERSION как атрибут класса
self.VERSION = VERSION
# Инициализация проверки обновлений
self.update_checker = UpdateChecker(
VERSION,
"https://gitea.filow.ru/LowaSC/ComConfigCopy"
)
# Настройка стиля
self.style = ttk.Style(self) self.style = ttk.Style(self)
self.style.theme_use("clam") self.style.theme_use("clam")
default_font = ("Segoe UI", 10) default_font = ("Segoe UI", 10)
self.option_add("*Font", default_font) self.option_add("*Font", default_font)
self.settings = settings self.settings = settings
self.connection = None self.connection = None
self.tftp_server = None self.tftp_server = None
@@ -656,7 +674,87 @@ class SerialAppGUI(tk.Tk):
# Проверка первого запуска # Проверка первого запуска
self.check_first_run() self.check_first_run()
def create_menu(self):
menubar = tk.Menu(self)
self.config(menu=menubar)
# Меню "Файл"
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Файл", menu=file_menu)
file_menu.add_command(label="Настройки", command=self.open_settings)
file_menu.add_separator()
file_menu.add_command(label="Выход", command=self.quit)
# Меню "Справка"
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Справка", menu=help_menu)
help_menu.add_command(label="Документация", command=self.open_documentation)
help_menu.add_command(label="Проверить обновления", command=self.check_for_updates)
help_menu.add_separator()
help_menu.add_command(label="О программе", command=self.open_about)
def check_for_updates(self):
"""Проверка наличия обновлений"""
def on_update_check(update_available, error):
if error:
messagebox.showerror(
"Ошибка проверки обновлений",
f"Не удалось проверить наличие обновлений:\n{error}"
)
elif update_available:
release_info = self.update_checker.get_release_notes()
if release_info:
response = messagebox.askyesno(
"Доступно обновление",
f"Доступна новая версия {release_info['version']}!\n\n"
f"Изменения:\n{release_info['description']}\n\n"
"Хотите перейти на страницу загрузки?",
)
if response:
webbrowser.open(release_info["download_url"])
else:
messagebox.showerror(
"Ошибка",
"Не удалось получить информацию о новой версии"
)
else:
messagebox.showinfo(
"Проверка обновлений",
"У вас установлена последняя версия программы"
)
self.update_checker.check_updates(callback=on_update_check)
def on_settings_changed(self):
"""Обработчик изменения настроек"""
self.settings = settings_load()
def open_settings(self):
"""Открытие окна настроек"""
settings_window = SettingsWindow(self, self.settings, self.on_settings_changed)
settings_window.transient(self)
settings_window.grab_set()
def open_documentation(self):
"""Открытие документации"""
doc_path = os.path.join("docs", "user_manual.md")
if os.path.exists(doc_path):
try:
os.startfile(doc_path)
except AttributeError:
# Для Linux и MacOS
try:
import subprocess
subprocess.run(["xdg-open", doc_path])
except:
webbrowser.open(f"file://{os.path.abspath(doc_path)}")
else:
messagebox.showerror(
"Ошибка",
"Файл документации не найден"
)
def check_first_run(self): def check_first_run(self):
"""Проверка первого запуска программы""" """Проверка первого запуска программы"""
show_settings = False show_settings = False
@@ -687,31 +785,6 @@ class SerialAppGUI(tk.Tk):
if response: if response:
self.open_settings() self.open_settings()
def create_menu(self):
menubar = tk.Menu(self)
self.config(menu=menubar)
# Меню "Файл"
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Файл", menu=file_menu)
file_menu.add_command(label="Настройки", command=self.open_settings)
file_menu.add_separator()
file_menu.add_command(label="Выход", command=self.quit)
# Меню "Справка"
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Справка", menu=help_menu)
help_menu.add_command(label="О программе", command=self.open_about)
def open_settings(self):
settings_window = SettingsWindow(self, self.settings, self.on_settings_changed)
settings_window.transient(self)
settings_window.grab_set()
def on_settings_changed(self):
# Обновляем настройки в основном приложении
self.settings = settings_load()
def create_tabs(self): def create_tabs(self):
self.notebook = ttk.Notebook(self) self.notebook = ttk.Notebook(self)
self.notebook.pack(fill=BOTH, expand=True, padx=5, pady=5) self.notebook.pack(fill=BOTH, expand=True, padx=5, pady=5)

44
README.md Normal file
View File

@@ -0,0 +1,44 @@
# ComConfigCopy
Программа для копирования конфигураций на коммутаторы.
## Описание
ComConfigCopy - это утилита, разработанная для автоматизации процесса копирования конфигураций на сетевые коммутаторы. Программа предоставляет удобный графический интерфейс для управления процессом копирования и настройки параметров подключения.
## Основные возможности
- Копирование конфигураций на коммутаторы через COM-порт
- Поддержка различных скоростей подключения
- Автоматическое определение доступных COM-портов
- Возможность сохранения и загрузки настроек
- Автоматическое обновление через GitHub
## Системные требования
- Windows 7/8/10/11
- Python 3.8 или выше
- Доступ к COM-портам
## Установка
1. Скачайте последнюю версию программы из [репозитория](https://gitea.filow.ru/LowaSC/ComConfigCopy/releases)
2. Распакуйте архив в удобное место
3. Запустите файл `ComConfigCopy.exe`
## Использование
1. Выберите COM-порт из списка доступных
2. Настройте параметры подключения (скорость, биты данных и т.д.)
3. Выберите файл конфигурации для отправки
4. Нажмите кнопку "Отправить" для начала процесса копирования
## Контакты
- Email: LowaWorkMail@gmail.com
- Telegram: [@LowaSC](https://t.me/LowaSC)
- Репозиторий: [ComConfigCopy](https://gitea.filow.ru/LowaSC/ComConfigCopy)
## Лицензия
Этот проект распространяется под лицензией MIT. Подробности смотрите в файле [LICENSE](LICENSE).

View File

@@ -2,19 +2,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import tkinter as tk import tkinter as tk
from tkinter import ttk, BOTH, X, BOTTOM from tkinter import ttk, BOTH, X, BOTTOM, END
import webbrowser import webbrowser
class AboutWindow(tk.Toplevel): class AboutWindow(tk.Toplevel):
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.title("О программе") self.title("О программе")
self.geometry("400x300") self.geometry("600x500")
self.resizable(False, False) self.resizable(False, False)
# Создаем фрейм # Сохраняем ссылку на родительское окно
self.parent = parent
# Создаем фрейм для содержимого
about_frame = ttk.Frame(self, padding="20") about_frame = ttk.Frame(self, padding="20")
about_frame.pack(fill=BOTH, expand=True) about_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
# Заголовок # Заголовок
ttk.Label( ttk.Label(
@@ -33,7 +36,7 @@ class AboutWindow(tk.Toplevel):
# Версия # Версия
ttk.Label( ttk.Label(
about_frame, about_frame,
text="Версия 1.0", text=f"Версия {getattr(parent, 'VERSION', '1.0.0')}",
font=("Segoe UI", 10) font=("Segoe UI", 10)
).pack(pady=(0, 20)) ).pack(pady=(0, 20))
@@ -80,14 +83,14 @@ class AboutWindow(tk.Toplevel):
# Кнопка закрытия # Кнопка закрытия
ttk.Button( ttk.Button(
about_frame, self,
text="Закрыть", text="Закрыть",
command=self.destroy command=self.destroy
).pack(side=BOTTOM, pady=(20, 0)) ).pack(side=BOTTOM, pady=10)
# Центрируем окно # Центрируем окно
self.center_window() self.center_window()
def center_window(self): def center_window(self):
self.update_idletasks() self.update_idletasks()
width = self.winfo_width() width = self.winfo_width()
@@ -95,6 +98,6 @@ class AboutWindow(tk.Toplevel):
x = (self.winfo_screenwidth() // 2) - (width // 2) x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2) y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}") self.geometry(f"{width}x{height}+{x}+{y}")
def open_url(self, url): def open_url(self, url):
webbrowser.open(url) webbrowser.open(url)

View File

@@ -1 +1,4 @@
tftpy>=0.8.0 tftpy>=0.8.0
pyserial>=3.5
requests>=2.31.0
packaging>=23.2

175
update_checker.py Normal file
View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import logging
import requests
import threading
from packaging import version
class UpdateCheckError(Exception):
"""Исключение для ошибок проверки обновлений"""
pass
class UpdateChecker:
"""Класс для проверки обновлений программы"""
def __init__(self, current_version, repo_url):
self.current_version = current_version
self.repo_url = repo_url
# Формируем базовый URL API
self.api_url = repo_url.replace("gitea.filow.ru", "gitea.filow.ru/api/v1/repos/LowaSC/ComConfigCopy")
self._update_available = False
self._latest_version = None
self._latest_release = None
self._error = None
self._changelog = None
def get_changelog(self, callback=None):
"""
Получение changelog из репозитория.
:param callback: Функция обратного вызова, которая будет вызвана после получения changelog
"""
def fetch():
try:
# Пытаемся получить CHANGELOG.md из репозитория
response = requests.get(f"{self.api_url}/contents/CHANGELOG.md", timeout=10)
response.raise_for_status()
content = response.json()
if "content" in content:
import base64
changelog_content = base64.b64decode(content["content"]).decode("utf-8")
self._changelog = changelog_content
self._error = None
else:
raise UpdateCheckError("Не удалось получить содержимое CHANGELOG.md")
except requests.RequestException as e:
error_msg = f"Ошибка получения changelog: {e}"
logging.error(error_msg, exc_info=True)
self._error = error_msg
self._changelog = None
except Exception as e:
error_msg = f"Неизвестная ошибка при получении changelog: {e}"
logging.error(error_msg, exc_info=True)
self._error = error_msg
self._changelog = None
finally:
if callback:
callback(self._changelog, self._error)
# Запускаем получение в отдельном потоке
threading.Thread(target=fetch, daemon=True).start()
def check_updates(self, callback=None):
"""
Проверка наличия обновлений.
:param callback: Функция обратного вызова, которая будет вызвана после проверки
"""
def check():
try:
response = requests.get(f"{self.api_url}/releases", timeout=10)
response.raise_for_status()
releases = response.json()
if not releases:
raise UpdateCheckError("Не найдено релизов в репозитории")
latest_release = releases[0]
latest_version = latest_release.get("tag_name", "").lstrip("v")
if not latest_version:
raise UpdateCheckError("Не удалось определить версию последнего релиза")
try:
if version.parse(latest_version) > version.parse(self.current_version):
self._update_available = True
self._latest_version = latest_version
self._latest_release = latest_release
logging.info(f"Доступно обновление: {latest_version}")
else:
logging.info("Обновления не требуются")
except version.InvalidVersion as e:
raise UpdateCheckError(f"Некорректный формат версии: {e}")
self._error = None
except requests.RequestException as e:
error_msg = f"Ошибка сетевого подключения: {e}"
logging.error(error_msg, exc_info=True)
self._error = error_msg
except UpdateCheckError as e:
logging.error(str(e), exc_info=True)
self._error = str(e)
except Exception as e:
error_msg = f"Неизвестная ошибка при проверке обновлений: {e}"
logging.error(error_msg, exc_info=True)
self._error = error_msg
finally:
if callback:
callback(self._update_available, self._error)
@property
def update_available(self):
"""Доступно ли обновление"""
return self._update_available
@property
def latest_version(self):
"""Последняя доступная версия"""
return self._latest_version
@property
def error(self):
"""Последняя ошибка при проверке обновлений"""
return self._error
@property
def changelog(self):
"""Текущий changelog"""
return self._changelog
def get_release_notes(self):
"""Получение информации о последнем релизе"""
if self._latest_release:
return {
"version": self._latest_version,
"description": self._latest_release.get("body", ""),
"download_url": self._latest_release.get("assets", [{}])[0].get("browser_download_url", "")
}
return None
def get_releases(self, callback=None):
"""
Получение списка релизов из репозитория.
:param callback: Функция обратного вызова, которая будет вызвана после получения списка релизов
"""
def fetch():
try:
response = requests.get(f"{self.api_url}/releases", timeout=10)
response.raise_for_status()
releases = response.json()
if not releases:
raise UpdateCheckError("Не найдено релизов в репозитории")
self._error = None
if callback:
callback(releases, None)
except requests.RequestException as e:
error_msg = f"Ошибка сетевого подключения: {e}"
logging.error(error_msg, exc_info=True)
self._error = error_msg
if callback:
callback(None, error_msg)
except Exception as e:
error_msg = f"Ошибка при получении списка релизов: {e}"
logging.error(error_msg, exc_info=True)
self._error = error_msg
if callback:
callback(None, error_msg)
# Запускаем получение в отдельном потоке
threading.Thread(target=fetch, daemon=True).start()