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:
123
ComConfigCopy.py
123
ComConfigCopy.py
@@ -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
44
README.md
Normal 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).
|
||||||
@@ -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)
|
||||||
@@ -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
175
update_checker.py
Normal 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()
|
||||||
Reference in New Issue
Block a user