- обновлён простой режим: тест цвета и проверка подсветки кнопок объединены в раздел «Тест подсветки» - добавлена проверка подсветки кнопок через 8-й силовой выход с автоматическим коротким тестом - упрощены описания пользовательских проверок и убраны лишние технические подробности из простого режима - изменена раскладка теста цвета, увеличена длительность шага до 1 секунды и убраны описания из PRO-блока - добавлена защита выпадающих списков от случайного изменения колесом мыши - улучшены немодальные PRO-диалоги, настройки соединения и обработка событий COM-порта - обновлены визуальные детали приложения, пользовательские ресурсы, инструкции AGENTS и Release Notes 1.3.0
175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
"""Диалог «Ввод команд» — мини-терминал для ручного общения с платой.
|
||
|
||
Позволяет отправлять произвольные команды и видеть ответы.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import Qt
|
||
from PySide6.QtGui import QKeyEvent
|
||
from PySide6.QtWidgets import (
|
||
QHBoxLayout,
|
||
QLineEdit,
|
||
QPushButton,
|
||
QTextEdit,
|
||
QVBoxLayout,
|
||
)
|
||
|
||
from ...services.com_service import ComConnection
|
||
from ...ui.icons import get_icon_muted, icon
|
||
from .styled_dialog import StyledDialog
|
||
|
||
|
||
class CommandTerminalDialog(StyledDialog):
|
||
def __init__(self, connection: ComConnection, parent=None) -> None:
|
||
super().__init__(parent)
|
||
self._conn = connection
|
||
self.setWindowTitle("Ввод команд")
|
||
self.setMinimumSize(540, 400)
|
||
|
||
self._setup_ui()
|
||
self._conn.line_received.connect(
|
||
self._on_line_received, Qt.ConnectionType.QueuedConnection
|
||
)
|
||
|
||
# История команд (макс. 50)
|
||
self._history: list[str] = []
|
||
self._history_index: int = -1
|
||
self._history_draft: str = ""
|
||
self._MAX_HISTORY = 50
|
||
|
||
# Перехватываем клавиши ↑↓ в поле ввода
|
||
self._input.installEventFilter(self)
|
||
|
||
# Фокус на поле ввода при открытии
|
||
self._input.setFocus()
|
||
|
||
def _setup_ui(self) -> None:
|
||
root = self.containerLayout()
|
||
root.setSpacing(8)
|
||
|
||
# ── Лог ──
|
||
self._log = QTextEdit()
|
||
self._log.setReadOnly(True)
|
||
self._log.setStyleSheet(
|
||
"QTextEdit { background: #1e293b; color: #e2e8f0; font-family: 'Consolas', 'Courier New', monospace; font-size: 12px; border-radius: 8px; padding: 8px; }"
|
||
)
|
||
root.addWidget(self._log, 1)
|
||
|
||
# ── Ввод ──
|
||
input_layout = QHBoxLayout()
|
||
self._input = QLineEdit()
|
||
self._input.setPlaceholderText("Введите команду (напр. 99, 05, 91, 8003)…")
|
||
self._input.returnPressed.connect(self._send_command)
|
||
input_layout.addWidget(self._input, 1)
|
||
|
||
send_btn = QPushButton("Отправить")
|
||
send_btn.setProperty("role", "primary")
|
||
send_btn.setIcon(icon("fa5s.paper-plane", color="#ecfdf5"))
|
||
send_btn.clicked.connect(self._send_command)
|
||
input_layout.addWidget(send_btn)
|
||
|
||
clear_btn = QPushButton("Очистить")
|
||
clear_btn.setIcon(icon("fa5s.eraser", color=get_icon_muted()))
|
||
clear_btn.clicked.connect(self._log.clear)
|
||
input_layout.addWidget(clear_btn)
|
||
|
||
root.addLayout(input_layout)
|
||
|
||
# ── Быстрые команды ──
|
||
quick_layout = QHBoxLayout()
|
||
quick_commands = [
|
||
("Статус (99)", "99"),
|
||
("Версия (05)", "05"),
|
||
("MAC (06)", "06"),
|
||
("Входы (91)", "91"),
|
||
("Сброс (00)", "00"),
|
||
]
|
||
for label, cmd in quick_commands:
|
||
btn = QPushButton(label)
|
||
btn.setMinimumHeight(30)
|
||
btn.setStyleSheet("padding: 4px 10px;")
|
||
btn.clicked.connect(lambda checked=False, c=cmd: self._send(c))
|
||
quick_layout.addWidget(btn)
|
||
quick_layout.addStretch()
|
||
root.addLayout(quick_layout)
|
||
|
||
# ── Закрыть ──
|
||
btn_layout = QHBoxLayout()
|
||
btn_layout.addStretch()
|
||
close_btn = QPushButton("Закрыть")
|
||
close_btn.setIcon(icon("fa5s.times", color=get_icon_muted()))
|
||
close_btn.clicked.connect(self.close)
|
||
btn_layout.addWidget(close_btn)
|
||
root.addLayout(btn_layout)
|
||
|
||
def _send_command(self) -> None:
|
||
text = self._input.text().strip()
|
||
if not text:
|
||
return
|
||
# Сохранить в историю
|
||
if not self._history or self._history[-1] != text:
|
||
self._history.append(text)
|
||
if len(self._history) > self._MAX_HISTORY:
|
||
self._history.pop(0)
|
||
self._history_index = -1
|
||
self._history_draft = ""
|
||
|
||
self._send(text)
|
||
self._input.clear()
|
||
|
||
def eventFilter(self, watched, event): # type: ignore[override]
|
||
if watched is self._input and isinstance(event, QKeyEvent):
|
||
if event.type() == QKeyEvent.Type.KeyPress:
|
||
key = event.key()
|
||
if key == Qt.Key.Key_Up:
|
||
self._history_navigate(-1)
|
||
return True
|
||
if key == Qt.Key.Key_Down:
|
||
self._history_navigate(1)
|
||
return True
|
||
return super().eventFilter(watched, event)
|
||
|
||
def _history_navigate(self, direction: int) -> None:
|
||
"""Перемещение по истории: direction=-1 (назад), +1 (вперёд)."""
|
||
if not self._history:
|
||
return
|
||
|
||
if self._history_index == -1:
|
||
# Сохраняем текущий ввод как черновик
|
||
self._history_draft = self._input.text()
|
||
|
||
new_index = self._history_index - direction
|
||
if new_index < -1:
|
||
new_index = -1
|
||
elif new_index >= len(self._history):
|
||
new_index = len(self._history) - 1
|
||
|
||
self._history_index = new_index
|
||
if new_index == -1:
|
||
self._input.setText(self._history_draft)
|
||
else:
|
||
self._input.setText(self._history[len(self._history) - 1 - new_index])
|
||
|
||
def _send(self, cmd: str) -> None:
|
||
if not self._conn.is_connected:
|
||
self._log.append('<span style="color:#f87171;">✗ Порт не подключён</span>')
|
||
return
|
||
try:
|
||
self._conn.send_async(cmd)
|
||
self._log.append(f'<span style="color:#4ade80;">→ {cmd}</span>')
|
||
except Exception as exc:
|
||
self._log.append(f'<span style="color:#f87171;">✗ {exc}</span>')
|
||
|
||
def _on_line_received(self, line: str) -> None:
|
||
stripped = line.strip()
|
||
if stripped:
|
||
self._log.append(f'<span style="color:#93c5fd;">← {stripped}</span>')
|
||
|
||
def closeEvent(self, event) -> None:
|
||
try:
|
||
self._conn.line_received.disconnect(self._on_line_received)
|
||
except (RuntimeError, TypeError):
|
||
pass
|
||
super().closeEvent(event)
|