Добавлен Конфигуратор v2

Собирает конфиг в один json файл и шифрует ключом FISHKArrCONFIG
This commit is contained in:
se_ga 2026-03-16 14:12:17 +03:00
parent bf27adedad
commit f7fab0f636
16 changed files with 1169 additions and 0 deletions

View File

@ -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'],
)

View File

@ -0,0 +1 @@
gAAAAABpt-F-qvW-lbBueT4nVH643T1At4REQXI8ZD-Ect7D6lE2lWz8-t2E3wLA1K92xUM5gjuAm7p66PysYz2M2KOoL7kPi9xCKPZTWfuqlYvqfy3N9qAO477gFxmMcX2N1MqVkymNcJkQwcawt5bXQKVjYZ8BN1OQGbSZtILBm0AmTzVewhjaw1BRjp0gqvN2qL1Ky7GkttPcLNV6F6a1n0Wc3g0iiRiLi7Qi9d-_IjbP4T0St76Qzii3JxOhZLXwgQrJD5wHrtsaeNTLM6GPPzk6StLD1_tXdJJJwTozJgH0502-VzQAOy-nKevtmvpqlp6VWDjB

BIN
Tools/ConfigappV2/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

BIN
Tools/ConfigappV2/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

17
Tools/ConfigappV2/main.py Normal file
View File

@ -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()

View File

@ -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,
"Ошибка",
"Не удалось сохранить конфигурацию"
)

View File

@ -0,0 +1,2 @@
PySide6==6.6.0
cryptography==41.0.7

View File

@ -0,0 +1,3 @@
from .encryption import ConfigManager
__all__ = ['ConfigManager']

View File

@ -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

View File

@ -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'
]

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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])

View File

@ -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