diff --git a/Tools/ConfigappV2/4Configurator.spec b/Tools/ConfigappV2/4Configurator.spec new file mode 100644 index 0000000..79288af --- /dev/null +++ b/Tools/ConfigappV2/4Configurator.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='4Configurator', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['logo.ico'], +) diff --git a/Tools/ConfigappV2/Config.rr b/Tools/ConfigappV2/Config.rr new file mode 100644 index 0000000..053c1e9 --- /dev/null +++ b/Tools/ConfigappV2/Config.rr @@ -0,0 +1 @@ +gAAAAABpt-F-qvW-lbBueT4nVH643T1At4REQXI8ZD-Ect7D6lE2lWz8-t2E3wLA1K92xUM5gjuAm7p66PysYz2M2KOoL7kPi9xCKPZTWfuqlYvqfy3N9qAO477gFxmMcX2N1MqVkymNcJkQwcawt5bXQKVjYZ8BN1OQGbSZtILBm0AmTzVewhjaw1BRjp0gqvN2qL1Ky7GkttPcLNV6F6a1n0Wc3g0iiRiLi7Qi9d-_IjbP4T0St76Qzii3JxOhZLXwgQrJD5wHrtsaeNTLM6GPPzk6StLD1_tXdJJJwTozJgH0502-VzQAOy-nKevtmvpqlp6VWDjB \ No newline at end of file diff --git a/Tools/ConfigappV2/logo.ico b/Tools/ConfigappV2/logo.ico new file mode 100644 index 0000000..1327d01 Binary files /dev/null and b/Tools/ConfigappV2/logo.ico differ diff --git a/Tools/ConfigappV2/logo.png b/Tools/ConfigappV2/logo.png new file mode 100644 index 0000000..5b10b66 Binary files /dev/null and b/Tools/ConfigappV2/logo.png differ diff --git a/Tools/ConfigappV2/main.py b/Tools/ConfigappV2/main.py new file mode 100644 index 0000000..bc50b1d --- /dev/null +++ b/Tools/ConfigappV2/main.py @@ -0,0 +1,17 @@ +import sys +from PySide6.QtWidgets import QApplication +from main_window import MainWindow + + +def main(): + app = QApplication(sys.argv) + app.setStyle('Fusion') + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Tools/ConfigappV2/main_window.py b/Tools/ConfigappV2/main_window.py new file mode 100644 index 0000000..357c7a9 --- /dev/null +++ b/Tools/ConfigappV2/main_window.py @@ -0,0 +1,166 @@ +import json +from typing import Dict, Any +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMessageBox, + QLabel, QHBoxLayout +) +from PySide6.QtGui import QIcon + +from utils.encryption import ConfigManager +from widgets.serial_widget import SerialNumberWidget +from widgets.light_widget import LightVersionWidget +from widgets.hopper_widget import HopperSensorWidget +from widgets.webserver_widget import WebServerWidget +from widgets.preview_widget import PreviewWidget + +class MainWindow(QMainWindow): + """Главное окно приложения""" + + def __init__(self): + super().__init__() + self.config_manager = ConfigManager() + self.init_ui() + self.load_current_config() + + self.setWindowIcon(QIcon("logo.png")) + + # Подписываемся на изменения во всех виджетах + self.setup_value_changed_connections() + + def init_ui(self): + self.setWindowTitle("Конфигуратор аппарат 4 в ряд") + self.setGeometry(100, 100, 650, 850) + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Основной layout + main_layout = QVBoxLayout(central_widget) + + # Информационная панель с путем сохранения + info_layout = QHBoxLayout() + info_layout.addWidget(QLabel("📁 Файл конфигурации:")) + + path_label = QLabel(self.config_manager.get_config_path()) + path_label.setStyleSheet(""" + QLabel { + color: #666; + font-family: monospace; + font-size: 11px; + background-color: #f5f5f5; + padding: 3px 8px; + border-radius: 3px; + } + """) + path_label.setWordWrap(True) + info_layout.addWidget(path_label, 1) + + main_layout.addLayout(info_layout) + + # Создаем вкладки + tabs = QTabWidget() + tabs.setTabPosition(QTabWidget.TabPosition.North) + + # Вкладка 1: Серийный номер + self.serial_widget = SerialNumberWidget() + tabs.addTab(self.serial_widget, "Серийный номер") + + # Вкладка 2: Подсветка + self.light_widget = LightVersionWidget("Подсветка", default=2) + tabs.addTab(self.light_widget, "Подсветка") + + # Вкладка 3: Электроника + electronics_layout = QVBoxLayout() + + # Версия электроники + self.electronics_widget = LightVersionWidget( + "Версия электроники", + default=2, + options={1: "Версия 1", 2: "Версия 2"} + ) + electronics_layout.addWidget(self.electronics_widget) + + # Датчик хоппера + self.hopper_widget = HopperSensorWidget() + electronics_layout.addWidget(self.hopper_widget) + + electronics_tab = QWidget() + electronics_tab.setLayout(electronics_layout) + tabs.addTab(electronics_tab, "Электроника") + + # Вкладка 4: Веб-сервер + self.webserver_widget = WebServerWidget() + tabs.addTab(self.webserver_widget, "Веб-сервер") + + # Вкладка 5: Сохранение + self.preview_widget = PreviewWidget(self.config_manager) + self.preview_widget.set_get_config_callback(self.get_current_config) + self.preview_widget.save_button.clicked.connect(self.save_config) + tabs.addTab(self.preview_widget, "Сохранение") + + main_layout.addWidget(tabs) + + def setup_value_changed_connections(self): + """Настраивает соединения для обновления предпросмотра""" + self.serial_widget.valueChanged.connect(self.on_config_changed) + self.light_widget.valueChanged.connect(self.on_config_changed) + self.electronics_widget.valueChanged.connect(self.on_config_changed) + self.hopper_widget.valueChanged.connect(self.on_config_changed) + self.webserver_widget.valueChanged.connect(self.on_config_changed) + + def on_config_changed(self): + """Обработчик изменения конфигурации""" + self.preview_widget.update_preview() + + def get_current_config(self) -> Dict[str, Any]: + """Собирает текущие значения из виджетов""" + config = { + "serial": self.serial_widget.get_serial(), + "light-version": self.light_widget.get_value(), + "electronic-version": self.electronics_widget.get_value(), + "sensor-positive-value": self.hopper_widget.get_value(), + "start-server": self.webserver_widget.get_start_server(), + "server-password": self.webserver_widget.get_password() + } + return config + + def load_current_config(self): + """Загружает текущий конфиг в виджеты""" + config = self.config_manager.current_config + + if config: + if "serial" in config: + self.serial_widget.set_serial(config["serial"]) + + if "light-version" in config: + self.light_widget.set_value(config["light-version"]) + + if "electronic-version" in config: + self.electronics_widget.set_value(config["electronic-version"]) + + if "sensor-positive-value" in config: + self.hopper_widget.set_value(config["sensor-positive-value"]) + + if "start-server" in config: + self.webserver_widget.set_start_server(config["start-server"]) + + if "server-password" in config: + self.webserver_widget.set_password(config["server-password"]) + + def save_config(self): + """Сохраняет конфигурацию""" + config = self.get_current_config() + + if self.config_manager.save_config(config): + QMessageBox.information( + self, + "Успех", + f"Конфигурация успешно сохранена\n\nФайл: {self.config_manager.get_config_path()}" + ) + else: + QMessageBox.critical( + self, + "Ошибка", + "Не удалось сохранить конфигурацию" + ) \ No newline at end of file diff --git a/Tools/ConfigappV2/requirements.txt b/Tools/ConfigappV2/requirements.txt new file mode 100644 index 0000000..778122a --- /dev/null +++ b/Tools/ConfigappV2/requirements.txt @@ -0,0 +1,2 @@ +PySide6==6.6.0 +cryptography==41.0.7 \ No newline at end of file diff --git a/Tools/ConfigappV2/utils/__init__.py b/Tools/ConfigappV2/utils/__init__.py new file mode 100644 index 0000000..b08de9d --- /dev/null +++ b/Tools/ConfigappV2/utils/__init__.py @@ -0,0 +1,3 @@ +from .encryption import ConfigManager + +__all__ = ['ConfigManager'] \ No newline at end of file diff --git a/Tools/ConfigappV2/utils/encryption.py b/Tools/ConfigappV2/utils/encryption.py new file mode 100644 index 0000000..407a3e0 --- /dev/null +++ b/Tools/ConfigappV2/utils/encryption.py @@ -0,0 +1,126 @@ +import json +import os +import base64 +from typing import Dict, Any +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +class ConfigManager: + """Класс для работы с зашифрованным конфигом""" + + def __init__(self, key_string: str = "FISHKArrCONFIG", config_file: str = "Config.rr"): + self.key_string = key_string + self.config_file = config_file + self.config_dir = self._get_config_directory() + self.config_path = os.path.join(self.config_dir, config_file) + self.current_config = self.load_config() + self._observers = [] + + def _get_config_directory(self) -> str: + """Возвращает путь к директории для сохранения конфига""" + # Путь: C:\Users\[USERNAME]\AppData\LocalLow\Robotic Retailers\Robonect4 + appdata = os.environ.get('LOCALAPPDATA', '') + + if appdata: + # Заменяем 'Local' на 'LocalLow' в пути + # Обычно LOCALAPPDATA указывает на ...\AppData\Local + # Нам нужно ...\AppData\LocalLow + base_path = appdata.replace('Local', 'LocalLow') + config_dir = os.path.join(base_path, 'Robotic Retailers', 'Robonect4') + else: + # Fallback для Linux/Mac + base_path = os.path.expanduser('~') + config_dir = os.path.join(base_path, '.config', 'Robotic Retailers', 'Robonect4') + + # Создаем директорию, если она не существует + try: + os.makedirs(config_dir, exist_ok=True) + print(f"Директория для конфига: {config_dir}") + except Exception as e: + print(f"Ошибка создания директории: {e}") + # В случае ошибки используем текущую директорию + config_dir = os.getcwd() + + return config_dir + + def add_observer(self, callback): + """Добавляет наблюдателя для обновлений конфига""" + self._observers.append(callback) + + def notify_observers(self): + """Уведомляет наблюдателей об изменении конфига""" + for callback in self._observers: + callback() + + def _derive_key(self) -> bytes: + """Создает ключ Fernet из строки""" + # Соль фиксированная для обратной совместимости + salt = b'fishka_salt_1234' + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + ) + key = base64.urlsafe_b64encode(kdf.derive(self.key_string.encode())) + return key + + def encrypt_data(self, data: Dict[str, Any]) -> bytes: + """Шифрует словарь в строку JSON и затем шифрует""" + json_str = json.dumps(data, ensure_ascii=False, indent=2) + fernet = Fernet(self._derive_key()) + encrypted = fernet.encrypt(json_str.encode('utf-8')) + return encrypted + + def decrypt_data(self, encrypted_data: bytes) -> Dict[str, Any]: + """Расшифровывает данные""" + try: + fernet = Fernet(self._derive_key()) + decrypted = fernet.decrypt(encrypted_data) + return json.loads(decrypted.decode('utf-8')) + except Exception as e: + print(f"Ошибка расшифровки: {e}") + return {} + + def save_config(self, data: Dict[str, Any]) -> bool: + """Сохраняет конфиг в файл в AppData/LocalLow""" + try: + encrypted = self.encrypt_data(data) + + # Убеждаемся, что директория существует + os.makedirs(self.config_dir, exist_ok=True) + + # Сохраняем файл + with open(self.config_path, 'wb') as f: + f.write(encrypted) + + print(f"Конфиг сохранен: {self.config_path}") + self.current_config = data + self.notify_observers() + return True + except Exception as e: + print(f"Ошибка сохранения: {e}") + return False + + def load_config(self) -> Dict[str, Any]: + """Загружает конфиг из файла в AppData/LocalLow""" + if not os.path.exists(self.config_path): + print(f"Файл конфига не найден: {self.config_path}") + return {} + + try: + with open(self.config_path, 'rb') as f: + encrypted = f.read() + + config = self.decrypt_data(encrypted) + print(f"Конфиг загружен: {self.config_path}") + return config + except Exception as e: + print(f"Ошибка загрузки: {e}") + return {} + + def get_config_path(self) -> str: + """Возвращает полный путь к файлу конфига""" + return self.config_path \ No newline at end of file diff --git a/Tools/ConfigappV2/widgets/__init__.py b/Tools/ConfigappV2/widgets/__init__.py new file mode 100644 index 0000000..237a033 --- /dev/null +++ b/Tools/ConfigappV2/widgets/__init__.py @@ -0,0 +1,13 @@ +from .serial_widget import SerialNumberWidget +from .light_widget import LightVersionWidget +from .hopper_widget import HopperSensorWidget +from .webserver_widget import WebServerWidget +from .preview_widget import PreviewWidget + +__all__ = [ + 'SerialNumberWidget', + 'LightVersionWidget', + 'HopperSensorWidget', + 'WebServerWidget', + 'PreviewWidget' +] \ No newline at end of file diff --git a/Tools/ConfigappV2/widgets/hopper_widget.py b/Tools/ConfigappV2/widgets/hopper_widget.py new file mode 100644 index 0000000..030f3ec --- /dev/null +++ b/Tools/ConfigappV2/widgets/hopper_widget.py @@ -0,0 +1,55 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QGroupBox, + QRadioButton, QButtonGroup +) +from PySide6.QtCore import Signal + + +class HopperSensorWidget(QWidget): + """Виджет для выбора типа датчика хоппера""" + + valueChanged = Signal() + + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + + group_box = QGroupBox("Тип датчика хоппера") + group_layout = QVBoxLayout() + + self.button_group = QButtonGroup(self) + + self.radio_normally_open = QRadioButton("Нормально разомкнутый (по умолчанию)(китайский)") + self.radio_normally_open.setChecked(True) + self.button_group.addButton(self.radio_normally_open, 1) + + self.radio_normally_closed = QRadioButton("Нормально замкнутый (русский)") + self.button_group.addButton(self.radio_normally_closed, 2) + + group_layout.addWidget(self.radio_normally_open) + group_layout.addWidget(self.radio_normally_closed) + + group_box.setLayout(group_layout) + layout.addWidget(group_box) + + self.button_group.buttonToggled.connect(self.on_value_changed) + self.setLayout(layout) + + def on_value_changed(self): + """Обработчик изменения значения""" + self.valueChanged.emit() + + def get_value(self) -> str: + """Возвращает '0' для нормально разомкнутого, '1' для нормально замкнутого""" + return '0' if self.button_group.checkedId() == 1 else '1' + + def set_value(self, value: str): + """Устанавливает значение из строки '0' или '1'""" + if value == '0': + self.radio_normally_open.setChecked(True) + else: + self.radio_normally_closed.setChecked(True) \ No newline at end of file diff --git a/Tools/ConfigappV2/widgets/light_widget.py b/Tools/ConfigappV2/widgets/light_widget.py new file mode 100644 index 0000000..66a482b --- /dev/null +++ b/Tools/ConfigappV2/widgets/light_widget.py @@ -0,0 +1,53 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QGroupBox, + QRadioButton, QButtonGroup +) +from PySide6.QtCore import Signal + + +class LightVersionWidget(QWidget): + """Виджет для выбора версии подсветки""" + + valueChanged = Signal() + + def __init__(self, title: str = "Подсветка", default: int = 2, options=None): + super().__init__() + if options is None: + options = {1: "Версия 1 (старая)(только на самой первой фишке)", 2: "Версия 2 (новая, по умолчанию)"} + + self.options = options + self.init_ui(title, default) + + def init_ui(self, title: str, default: int): + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + + group_box = QGroupBox(title) + group_layout = QVBoxLayout() + + self.button_group = QButtonGroup(self) + self.radio_buttons = {} + + for value, label in self.options.items(): + radio = QRadioButton(label) + if value == default: + radio.setChecked(True) + self.button_group.addButton(radio, value) + self.radio_buttons[value] = radio + group_layout.addWidget(radio) + radio.toggled.connect(self.on_value_changed) + + group_box.setLayout(group_layout) + layout.addWidget(group_box) + self.setLayout(layout) + + def on_value_changed(self): + """Обработчик изменения значения""" + self.valueChanged.emit() + + def get_value(self) -> int: + return self.button_group.checkedId() + + def set_value(self, value: int): + if value in self.radio_buttons: + self.radio_buttons[value].setChecked(True) \ No newline at end of file diff --git a/Tools/ConfigappV2/widgets/preview_widget.py b/Tools/ConfigappV2/widgets/preview_widget.py new file mode 100644 index 0000000..049517e --- /dev/null +++ b/Tools/ConfigappV2/widgets/preview_widget.py @@ -0,0 +1,303 @@ +import json +from typing import Dict, Any, Callable +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, + QPushButton, QGroupBox, QGridLayout, QFrame, QScrollArea +) +from PySide6.QtGui import QFont, QPalette, QColor +from PySide6.QtCore import Qt, QTimer + + +class PreviewWidget(QWidget): + """Виджет предпросмотра конфига в удобочитаемом формате с темной темой""" + + def __init__(self, config_manager): + super().__init__() + self.config_manager = config_manager + self.get_config_callback = None + self.init_ui() + + # Подписываемся на обновления конфига + self.config_manager.add_observer(self.update_preview) + + # Используем QTimer для отложенного обновления после полной инициализации + QTimer.singleShot(100, self.initial_update) + + def init_ui(self): + layout = QVBoxLayout() + + # Заголовок + title_label = QLabel("Предпросмотр конфигурации") + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # Создаем область прокрутки для текущего конфига + current_group = QGroupBox("📁 Текущая конфигурация (из файла)") + current_layout = QVBoxLayout() + current_layout.setSpacing(5) + + # Область прокрутки для текущего конфига + current_scroll = QScrollArea() + current_scroll.setWidgetResizable(True) + current_scroll.setFrameShape(QFrame.NoFrame) + current_scroll.setMaximumHeight(250) + + self.current_content = QWidget() + self.current_content_layout = QVBoxLayout(self.current_content) + self.current_content_layout.setAlignment(Qt.AlignTop) + self.current_content_layout.setSpacing(5) + self.current_content_layout.setContentsMargins(5, 5, 5, 5) + + current_scroll.setWidget(self.current_content) + current_layout.addWidget(current_scroll) + + current_group.setLayout(current_layout) + layout.addWidget(current_group) + + # Разделитель + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Sunken) + layout.addWidget(separator) + + # Создаем область прокрутки для нового конфига + new_group = QGroupBox("✨ Новая конфигурация (будет сохранена)") + + new_layout = QVBoxLayout() + new_layout.setSpacing(5) + + # Область прокрутки для нового конфига + new_scroll = QScrollArea() + new_scroll.setWidgetResizable(True) + new_scroll.setFrameShape(QFrame.NoFrame) + new_scroll.setMaximumHeight(250) + + self.new_content = QWidget() + self.new_content_layout = QVBoxLayout(self.new_content) + self.new_content_layout.setAlignment(Qt.AlignTop) + self.new_content_layout.setSpacing(5) + self.new_content_layout.setContentsMargins(5, 5, 5, 5) + + new_scroll.setWidget(self.new_content) + new_layout.addWidget(new_scroll) + + new_group.setLayout(new_layout) + layout.addWidget(new_group) + + # Информационная панель + info_frame = QFrame() + + info_layout = QHBoxLayout(info_frame) + info_layout.setContentsMargins(10, 5, 10, 5) + + info_icon = QLabel("ℹ️") + info_layout.addWidget(info_icon) + + self.info_label = QLabel("Загрузка конфигурации...") + info_layout.addWidget(self.info_label) + info_layout.addStretch() + + layout.addWidget(info_frame) + + # Кнопка сохранения + self.save_button = QPushButton("💾 Сохранить конфигурацию") + layout.addWidget(self.save_button) + + self.setLayout(layout) + + def initial_update(self): + """Первоначальное обновление при загрузке виджета""" + if self.get_config_callback: + self.update_preview() + self.info_label.setText("✓ Конфигурация загружена") + + def set_get_config_callback(self, callback: Callable[[], Dict[str, Any]]): + """Устанавливает функцию для получения текущей конфигурации""" + self.get_config_callback = callback + # Сразу обновляем preview после установки колбэка + QTimer.singleShot(50, self.initial_update) + + def format_value(self, key: str, value: Any) -> str: + """Форматирует значение в зависимости от типа и ключа""" + if isinstance(value, bool): + return "✅ Да" if value else "❌ Нет" + elif isinstance(value, int): + if key in ['light-version', 'electronic-version']: + return f"⚡ Версия {value}" + return str(value) + elif isinstance(value, str): + if key == 'sensor-positive-value': + return "🔌 Нормально разомкнутый" if value == '0' else "🔌 Нормально замкнутый" + elif key == 'serial': + return f"🔢 {value}" + elif key == 'server-password': + if value: + return "🔑 " + "•" * len(value) + return "🔑 (не установлен)" + return value + return str(value) + + def get_russian_key(self, key: str) -> str: + """Возвращает русское название параметра""" + translations = { + 'serial': 'Серийный номер', + 'light-version': 'Версия подсветки', + 'electronic-version': 'Версия электроники', + 'sensor-positive-value': 'Тип датчика хоппера', + 'start-server': 'Запуск сайт-панели', + 'server-password': 'Пароль администратора' + } + return translations.get(key, key) + + def create_parameter_widget(self, key: str, value: Any, is_new: bool = False, is_changed: bool = False) -> QWidget: + """Создает виджет для отображения одного параметра с темной темой""" + widget = QWidget() + + # Определяем стиль в зависимости от типа (новый/текущий и измененный) + if is_new: + if is_changed: + # Измененный параметр в новой конфигурации + bg_color = "#1e3b2a" + border_color = "#f39c12" + border_style = "2px solid" + else: + # Новый параметр без изменений + bg_color = "#1a2f2a" + border_color = "#27ae60" + border_style = "1px solid" + else: + # Текущая конфигурация + bg_color = "#2c3e50" + border_color = "#7f8c8d" + border_style = "1px solid" + + + + layout = QHBoxLayout(widget) + layout.setContentsMargins(10, 8, 10, 8) + layout.setSpacing(10) + + # Название параметра + name_label = QLabel(self.get_russian_key(key)) + name_label.setAlignment(Qt.AlignLeft) + layout.addWidget(name_label) + + # Разделитель + separator = QLabel(":") + layout.addWidget(separator) + + # Значение + value_label = QLabel(self.format_value(key, value)) + value_label.setWordWrap(True) + value_label.setAlignment(Qt.AlignLeft) + layout.addWidget(value_label, 1) + + # Если параметр изменен, добавляем индикатор + if is_changed: + change_indicator = QLabel("✏️ изменено") + change_indicator.setAlignment(Qt.AlignRight) + layout.addWidget(change_indicator) + + return widget + + def update_preview(self): + """Обновляет предпросмотр в удобочитаемом формате с темной темой""" + # Очищаем текущее содержимое + self.clear_layout(self.current_content_layout) + self.clear_layout(self.new_content_layout) + + # Отображаем текущий конфиг + current_config = self.config_manager.current_config + + if current_config and current_config != {}: + # Сортируем ключи для консистентного отображения + keys_order = ['serial', 'light-version', 'electronic-version', + 'sensor-positive-value', 'start-server', 'server-password'] + + for key in keys_order: + if key in current_config: + param_widget = self.create_parameter_widget( + key, current_config[key], is_new=False, is_changed=False + ) + self.current_content_layout.addWidget(param_widget) + else: + # Сообщение об отсутствии конфига + empty_widget = QWidget() + empty_layout = QHBoxLayout(empty_widget) + + empty_label = QLabel("📭 Файл конфигурации не найден или пуст") + empty_label.setAlignment(Qt.AlignCenter) + empty_layout.addWidget(empty_label) + + self.current_content_layout.addWidget(empty_widget) + + # Добавляем растяжку в конец текущего конфига + self.current_content_layout.addStretch() + + # Отображаем новый конфиг + if self.get_config_callback: + new_config = self.get_config_callback() + + if new_config: + # Сортируем ключи для консистентного отображения + keys_order = ['serial', 'light-version', 'electronic-version', + 'sensor-positive-value', 'start-server', 'server-password'] + + changes_count = 0 + + for key in keys_order: + if key in new_config: + # Проверяем, отличается ли значение от текущего + is_different = ( + current_config and + key in current_config and + current_config[key] != new_config[key] + ) + + if is_different: + changes_count += 1 + + param_widget = self.create_parameter_widget( + key, new_config[key], is_new=True, is_changed=is_different + ) + + self.new_content_layout.addWidget(param_widget) + + # Добавляем растяжку в конец нового конфига + self.new_content_layout.addStretch() + + # Обновляем информационную панель + if current_config and current_config != {}: + if changes_count > 0: + self.info_label.setText(f"📊 Обнаружено изменений: {changes_count}") + else: + self.info_label.setText("✓ Изменений нет") + else: + self.info_label.setText("🆕 Будет создан новый файл конфигурации") + else: + # Пустой новый конфиг + empty_new_widget = QWidget() + empty_new_layout = QHBoxLayout(empty_new_widget) + + empty_new_label = QLabel("⚙️ Настройте параметры на других вкладках") + empty_new_label.setAlignment(Qt.AlignCenter) + empty_new_layout.addWidget(empty_new_label) + + self.new_content_layout.addWidget(empty_new_widget) + self.new_content_layout.addStretch() + + self.info_label.setText("👆 Настройте параметры на других вкладках") + + def clear_layout(self, layout): + """Очищает layout от всех виджетов""" + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + def showEvent(self, event): + """Переопределяем событие показа виджета для обновления при каждом открытии вкладки""" + super().showEvent(event) + # Обновляем preview при каждом показе вкладки + self.update_preview() \ No newline at end of file diff --git a/Tools/ConfigappV2/widgets/serial_widget.py b/Tools/ConfigappV2/widgets/serial_widget.py new file mode 100644 index 0000000..6c95742 --- /dev/null +++ b/Tools/ConfigappV2/widgets/serial_widget.py @@ -0,0 +1,314 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit +) +from PySide6.QtCore import Signal, Qt +from PySide6.QtGui import QRegularExpressionValidator +from PySide6.QtCore import QRegularExpression + + +class NumberLineEdit(QLineEdit): + """Специализированное поле для ввода только чисел""" + + def __init__(self, max_length: int, parent=None): + super().__init__(parent) + self.setMaxLength(max_length) + + # Создаем валидатор для цифр + regex = QRegularExpression("[0-9]*") + validator = QRegularExpressionValidator(regex) + self.setValidator(validator) + + +class SerialNumberWidget(QWidget): + """Виджет для ввода серийного номера (только цифры)""" + + valueChanged = Signal() + + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + layout.setSpacing(5) + + # Описание формата + desc_label = QLabel("Формат: Axxx/Dxxxxxxxx/Vxxx/Nxxx (только цифры)") + layout.addWidget(desc_label) + + # Контейнер для полей ввода + fields_container = QWidget() + fields_layout = QVBoxLayout(fields_container) + fields_layout.setSpacing(2) + fields_layout.setContentsMargins(0, 0, 0, 0) + + # Верхний ряд - метки полей + labels_layout = QHBoxLayout() + labels_layout.setSpacing(2) + + labels_layout.addSpacing(10) + + label_a = QLabel("A") + label_a.setFixedWidth(40) + label_a.setAlignment(Qt.AlignCenter) + labels_layout.addWidget(label_a) + + labels_layout.addSpacing(30) + + label_d = QLabel("D") + label_d.setFixedWidth(70) + label_d.setAlignment(Qt.AlignCenter) + labels_layout.addWidget(label_d) + + labels_layout.addSpacing(30) + + label_v = QLabel("V") + label_v.setFixedWidth(40) + label_v.setAlignment(Qt.AlignCenter) + labels_layout.addWidget(label_v) + + labels_layout.addSpacing(30) + + label_n = QLabel("N") + label_n.setFixedWidth(40) + label_n.setAlignment(Qt.AlignCenter) + labels_layout.addWidget(label_n) + + labels_layout.addStretch() + fields_layout.addLayout(labels_layout) + + # Ряд с полями ввода + input_layout = QHBoxLayout() + input_layout.setSpacing(2) + + input_layout.addSpacing(10) + + # Поле A + self.part_a = NumberLineEdit(3) + self.part_a.setFixedWidth(40) + self.part_a.setPlaceholderText("xxx") + self.part_a.setAlignment(Qt.AlignCenter) + self.part_a.textChanged.connect(self.validate_and_update) + self.part_a.returnPressed.connect(lambda: self.focus_next_field(self.part_a, self.part_d)) + input_layout.addWidget(self.part_a) + + input_layout.addSpacing(30) + + # Поле D + self.part_d = NumberLineEdit(8) + self.part_d.setFixedWidth(70) + self.part_d.setPlaceholderText("xxxxxxxx") + self.part_d.setAlignment(Qt.AlignCenter) + self.part_d.textChanged.connect(self.validate_and_update) + self.part_d.returnPressed.connect(lambda: self.focus_next_field(self.part_d, self.part_v)) + input_layout.addWidget(self.part_d) + + input_layout.addSpacing(30) + + # Поле V + self.part_v = NumberLineEdit(3) + self.part_v.setFixedWidth(40) + self.part_v.setPlaceholderText("xxx") + self.part_v.setAlignment(Qt.AlignCenter) + self.part_v.textChanged.connect(self.validate_and_update) + self.part_v.returnPressed.connect(lambda: self.focus_next_field(self.part_v, self.part_n)) + input_layout.addWidget(self.part_v) + + input_layout.addSpacing(30) + + # Поле N + self.part_n = NumberLineEdit(3) + self.part_n.setFixedWidth(40) + self.part_n.setPlaceholderText("xxx") + self.part_n.setAlignment(Qt.AlignCenter) + self.part_n.textChanged.connect(self.validate_and_update) + self.part_n.returnPressed.connect(lambda: self.focus_next_field(self.part_n, None)) + input_layout.addWidget(self.part_n) + + input_layout.addStretch() + fields_layout.addLayout(input_layout) + + # Нижний ряд - индикаторы заполнения + indicators_layout = QHBoxLayout() + indicators_layout.setSpacing(2) + + indicators_layout.addSpacing(10) + + self.indicator_a = QLabel("0/3") + self.indicator_a.setFixedWidth(40) + self.indicator_a.setAlignment(Qt.AlignCenter) + indicators_layout.addWidget(self.indicator_a) + + indicators_layout.addSpacing(30) + + self.indicator_d = QLabel("0/8") + self.indicator_d.setFixedWidth(70) + self.indicator_d.setAlignment(Qt.AlignCenter) + indicators_layout.addWidget(self.indicator_d) + + indicators_layout.addSpacing(30) + + self.indicator_v = QLabel("0/3") + self.indicator_v.setFixedWidth(40) + self.indicator_v.setAlignment(Qt.AlignCenter) + indicators_layout.addWidget(self.indicator_v) + + indicators_layout.addSpacing(30) + + self.indicator_n = QLabel("0/3") + self.indicator_n.setFixedWidth(40) + self.indicator_n.setAlignment(Qt.AlignCenter) + indicators_layout.addWidget(self.indicator_n) + + indicators_layout.addStretch() + fields_layout.addLayout(indicators_layout) + + layout.addWidget(fields_container) + + # Предпросмотр с темным стилем + preview_label = QLabel("Результирующий серийный номер:") + preview_label.setStyleSheet("font-weight: bold; margin-top: 15px;") + layout.addWidget(preview_label) + + self.preview = QLabel() + self.preview.setStyleSheet(""" + QLabel { + font-family: monospace; + font-size: 16px; + padding: 12px; + background-color: #2b2b2b; + color: #00ff00; + border: 2px solid #555; + border-radius: 5px; + font-weight: bold; + } + """) + self.preview.setAlignment(Qt.AlignCenter) + layout.addWidget(self.preview) + + # Статус заполнения (без стилей) + self.status_label = QLabel() + self.status_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.status_label) + + layout.addStretch() + self.setLayout(layout) + + # Первоначальное обновление + self.update_preview() + + def focus_next_field(self, current_field, next_field): + """Перемещает фокус на следующее поле при нажатии Enter""" + if next_field: + next_field.setFocus() + + def validate_and_update(self): + """Проверяет ввод и обновляет предпросмотр""" + # Автоматический переход при полном заполнении поля + sender = self.sender() + if sender: + text = sender.text() + max_length = sender.maxLength() + + if len(text) == max_length: + if sender == self.part_a: + self.part_d.setFocus() + elif sender == self.part_d: + self.part_v.setFocus() + elif sender == self.part_v: + self.part_n.setFocus() + elif sender == self.part_n: + self.part_n.clearFocus() + + self.update_preview() + self.valueChanged.emit() + + def update_preview(self): + """Обновляет предпросмотр и индикаторы""" + a_text = self.part_a.text() + d_text = self.part_d.text() + v_text = self.part_v.text() + n_text = self.part_n.text() + + # Обновляем индикаторы + self.indicator_a.setText(f"{len(a_text)}/3") + self.indicator_d.setText(f"{len(d_text)}/8") + self.indicator_v.setText(f"{len(v_text)}/3") + self.indicator_n.setText(f"{len(n_text)}/3") + + # Формируем серийный номер + a = a_text if len(a_text) == 3 else a_text.ljust(3, 'x') + d = d_text if len(d_text) == 8 else d_text.ljust(8, 'x') + v = v_text if len(v_text) == 3 else v_text.ljust(3, 'x') + n = n_text if len(n_text) == 3 else n_text.ljust(3, 'x') + + serial = f"A{a}/D{d}/V{v}/N{n}" + self.preview.setText(serial) + + # Проверяем полноту заполнения + all_filled = len(a_text) == 3 and len(d_text) == 8 and len(v_text) == 3 and len(n_text) == 3 + + if all_filled: + self.status_label.setText("✓ Все поля заполнены корректно") + else: + missing = [] + if len(a_text) < 3: missing.append("A") + if len(d_text) < 8: missing.append("D") + if len(v_text) < 3: missing.append("V") + if len(n_text) < 3: missing.append("N") + + if missing: + self.status_label.setText( + f"Не заполнены поля: {', '.join(missing)} (недостающие символы будут заменены на 'x')" + ) + else: + self.status_label.setText("") + + def get_serial(self) -> str: + """Возвращает серийный номер с заполнением недостающих символов 'x'""" + a = self.part_a.text().ljust(3, 'x')[:3] + d = self.part_d.text().ljust(8, 'x')[:8] + v = self.part_v.text().ljust(3, 'x')[:3] + n = self.part_n.text().ljust(3, 'x')[:3] + + return f"A{a}/D{d}/V{v}/N{n}" + + def set_serial(self, serial: str): + """Устанавливает серийный номер из строки""" + try: + import re + pattern = r'A([^/]+)/D([^/]+)/V([^/]+)/N(.+)' + match = re.match(pattern, serial) + + if match: + a, d, v, n = match.groups() + + a = a.replace('A', '') if a.startswith('A') else a + d = d.replace('D', '') if d.startswith('D') else d + v = v.replace('V', '') if v.startswith('V') else v + n = n.replace('N', '') if n.startswith('N') else n + + a = a.rstrip('x') + d = d.rstrip('x') + v = v.rstrip('x') + n = n.rstrip('x') + + a = ''.join(c for c in a if c.isdigit()) + d = ''.join(c for c in d if c.isdigit()) + v = ''.join(c for c in v if c.isdigit()) + n = ''.join(c for c in n if c.isdigit()) + + self.part_a.setText(a) + self.part_d.setText(d) + self.part_v.setText(v) + self.part_n.setText(n) + except Exception as e: + print(f"Ошибка парсинга серийного номера: {e}") + + def clear(self): + """Очищает все поля""" + self.part_a.clear() + self.part_d.clear() + self.part_v.clear() + self.part_n.clear() \ No newline at end of file diff --git a/Tools/ConfigappV2/widgets/webserver_widget.py b/Tools/ConfigappV2/widgets/webserver_widget.py new file mode 100644 index 0000000..a2248e3 --- /dev/null +++ b/Tools/ConfigappV2/widgets/webserver_widget.py @@ -0,0 +1,74 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QCheckBox, QGroupBox, QFormLayout +) +from PySide6.QtCore import Signal + + +class WebServerWidget(QWidget): + """Виджет настроек веб-сервера""" + + valueChanged = Signal() + + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + + # Чекбокс запуска сайта + self.start_server_check = QCheckBox("Запускать сайт-панель") + self.start_server_check.toggled.connect(self.on_value_changed) + layout.addWidget(self.start_server_check) + + # Пароль администратора + group_box = QGroupBox("Пароль администратора") + group_layout = QFormLayout() + + self.password_edit = QLineEdit() + self.password_edit.setMaxLength(10) + self.password_edit.setPlaceholderText("до 10 символов") + self.password_edit.textChanged.connect(self.on_value_changed) + + # Индикатор длины пароля + self.length_label = QLabel("0/10") + self.length_label.setStyleSheet("color: #666; font-size: 10px;") + + password_layout = QHBoxLayout() + password_layout.addWidget(self.password_edit) + password_layout.addWidget(self.length_label) + + group_layout.addRow("Пароль:", password_layout) + group_box.setLayout(group_layout) + layout.addWidget(group_box) + + # Обновляем индикатор длины + self.password_edit.textChanged.connect(self.update_length_indicator) + + self.setLayout(layout) + + def on_value_changed(self): + """Обработчик изменения значения""" + self.valueChanged.emit() + + def update_length_indicator(self): + length = len(self.password_edit.text()) + self.length_label.setText(f"{length}/10") + + if length > 10: + self.length_label.setStyleSheet("color: red; font-size: 10px;") + else: + self.length_label.setStyleSheet("color: green; font-size: 10px;") + + def get_start_server(self) -> bool: + return self.start_server_check.isChecked() + + def get_password(self) -> str: + return self.password_edit.text() + + def set_start_server(self, value: bool): + self.start_server_check.setChecked(value) + + def set_password(self, password: str): + self.password_edit.setText(password[:10]) \ No newline at end of file diff --git a/Tools/ConfigappV2/сборка.txt b/Tools/ConfigappV2/сборка.txt new file mode 100644 index 0000000..f7c744f --- /dev/null +++ b/Tools/ConfigappV2/сборка.txt @@ -0,0 +1,3 @@ +pyinstaller --onefile --windowed --icon=app.ico --name="Robonect4_Configurator" main.py +pyinstaller --onefile --windowed --icon=logo.png --name="4Configurator" main.py +pyinstaller --onefile --windowed --icon=logo.ico --name="4Configurator" main.py \ No newline at end of file