Добавлен Конфигуратор v2
Собирает конфиг в один json файл и шифрует ключом FISHKArrCONFIG
This commit is contained in:
parent
bf27adedad
commit
f7fab0f636
39
Tools/ConfigappV2/4Configurator.spec
Normal file
39
Tools/ConfigappV2/4Configurator.spec
Normal 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'],
|
||||
)
|
||||
1
Tools/ConfigappV2/Config.rr
Normal file
1
Tools/ConfigappV2/Config.rr
Normal file
@ -0,0 +1 @@
|
||||
gAAAAABpt-F-qvW-lbBueT4nVH643T1At4REQXI8ZD-Ect7D6lE2lWz8-t2E3wLA1K92xUM5gjuAm7p66PysYz2M2KOoL7kPi9xCKPZTWfuqlYvqfy3N9qAO477gFxmMcX2N1MqVkymNcJkQwcawt5bXQKVjYZ8BN1OQGbSZtILBm0AmTzVewhjaw1BRjp0gqvN2qL1Ky7GkttPcLNV6F6a1n0Wc3g0iiRiLi7Qi9d-_IjbP4T0St76Qzii3JxOhZLXwgQrJD5wHrtsaeNTLM6GPPzk6StLD1_tXdJJJwTozJgH0502-VzQAOy-nKevtmvpqlp6VWDjB
|
||||
BIN
Tools/ConfigappV2/logo.ico
Normal file
BIN
Tools/ConfigappV2/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
BIN
Tools/ConfigappV2/logo.png
Normal file
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
17
Tools/ConfigappV2/main.py
Normal 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()
|
||||
166
Tools/ConfigappV2/main_window.py
Normal file
166
Tools/ConfigappV2/main_window.py
Normal 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,
|
||||
"Ошибка",
|
||||
"Не удалось сохранить конфигурацию"
|
||||
)
|
||||
2
Tools/ConfigappV2/requirements.txt
Normal file
2
Tools/ConfigappV2/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
PySide6==6.6.0
|
||||
cryptography==41.0.7
|
||||
3
Tools/ConfigappV2/utils/__init__.py
Normal file
3
Tools/ConfigappV2/utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .encryption import ConfigManager
|
||||
|
||||
__all__ = ['ConfigManager']
|
||||
126
Tools/ConfigappV2/utils/encryption.py
Normal file
126
Tools/ConfigappV2/utils/encryption.py
Normal 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
|
||||
13
Tools/ConfigappV2/widgets/__init__.py
Normal file
13
Tools/ConfigappV2/widgets/__init__.py
Normal 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'
|
||||
]
|
||||
55
Tools/ConfigappV2/widgets/hopper_widget.py
Normal file
55
Tools/ConfigappV2/widgets/hopper_widget.py
Normal 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)
|
||||
53
Tools/ConfigappV2/widgets/light_widget.py
Normal file
53
Tools/ConfigappV2/widgets/light_widget.py
Normal 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)
|
||||
303
Tools/ConfigappV2/widgets/preview_widget.py
Normal file
303
Tools/ConfigappV2/widgets/preview_widget.py
Normal 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()
|
||||
314
Tools/ConfigappV2/widgets/serial_widget.py
Normal file
314
Tools/ConfigappV2/widgets/serial_widget.py
Normal 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()
|
||||
74
Tools/ConfigappV2/widgets/webserver_widget.py
Normal file
74
Tools/ConfigappV2/widgets/webserver_widget.py
Normal 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])
|
||||
3
Tools/ConfigappV2/сборка.txt
Normal file
3
Tools/ConfigappV2/сборка.txt
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user