Files
templates/python/pysetterminal/serialbus.py

782 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import os
import weakref
from functools import partial
from copy import deepcopy
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class _MacroActionIcon(QIconEngine):
"""Scalable action icons, painted with the button's current theme color."""
def __init__(self, action, button):
super().__init__()
self.action = action
self.button = weakref.ref(button)
def clone(self):
return _MacroActionIcon(self.action, self.button())
def paint(self, painter, rect, mode, state):
button = self.button()
palette = button.palette() if button is not None else QApplication.palette()
group = QPalette.Disabled if mode == QIcon.Disabled else QPalette.Active
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
side = min(rect.width(), rect.height())
painter.translate(rect.x() + (rect.width() - side) / 2, rect.y() + (rect.height() - side) / 2)
painter.scale(side / 24, side / 24)
painter.setPen(QPen(palette.color(group, QPalette.ButtonText), 1.8, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.setBrush(Qt.NoBrush)
if self.action == "edit":
path = QPainterPath(QPointF(4, 16))
for x, y in ((15, 5), (19, 9), (8, 20), (3, 21), (4, 16)):
path.lineTo(x, y)
painter.drawPath(path)
painter.drawLine(QPointF(13, 7), QPointF(17, 11))
else:
painter.drawLine(QPointF(4, 6), QPointF(20, 6))
painter.drawRoundedRect(QRectF(6, 6, 12, 15), 1.5, 1.5)
painter.drawPolyline(QPolygonF([QPointF(9, 6), QPointF(9, 3), QPointF(15, 3), QPointF(15, 6)]))
painter.drawLine(QPointF(10, 10), QPointF(10, 17))
painter.drawLine(QPointF(14, 10), QPointF(14, 17))
painter.restore()
def pixmap(self, size, mode, state):
pixmap = QPixmap(size)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
self.paint(painter, QRect(QPoint(), size), mode, state)
painter.end()
return pixmap
class SerialBus:
"""ВЫСОКОУРОВНЕВЫЙ протокол для работы с серийной шиной (только через регистры)"""
def __init__(self, raw_protocol):
self.raw = raw_protocol # Используем низкоуровневый протокол
def _execute(self, write, tetrad, register, data, device_address):
from protocan.tms_terminal import bus_steps
try:
steps = bus_steps(write, tetrad, register, data)
except (ValueError, RuntimeError) as error:
return False, str(error)
result = None
for address, value in steps:
success, result = (self.raw.peek(address, device_address) if value is None
else self.raw.poke(address, value, device_address))
if not success:
return False, result
return True, "Запись в шину подтверждена" if write else result
def write_bus(self, data_word, address_tetrad, register_num=0, device_address=1):
return self._execute(True, address_tetrad, register_num, data_word, device_address)
def read_bus(self, address_tetrad, register_num=0, device_address=1):
return self._execute(False, address_tetrad, register_num, 0, device_address)
class MacroCommandDialog(QDialog):
"""Диалог для добавления/редактирования команды макроса"""
def __init__(self, parent=None, command=None):
super().__init__(parent)
self.command = command if command else {}
self.init_ui()
def init_ui(self):
self.setWindowTitle("Команда макроса")
self.setMinimumWidth(400)
layout = QVBoxLayout()
# Тип команды
type_layout = QHBoxLayout()
type_layout.addWidget(QLabel("Тип команды:"))
self.cmd_type = QComboBox()
self.cmd_type.addItems(["Запись", "Чтение"])
type_layout.addWidget(self.cmd_type)
layout.addLayout(type_layout)
# Адрес устройства
addr_layout = QHBoxLayout()
addr_layout.addWidget(QLabel("Адрес устройства (0-14):"))
self.device_addr = QSpinBox()
self.device_addr.setRange(0, 14)
addr_layout.addWidget(self.device_addr)
layout.addLayout(addr_layout)
# Регистр
reg_layout = QHBoxLayout()
reg_layout.addWidget(QLabel("Регистр (0-15):"))
self.register_num = QSpinBox()
self.register_num.setRange(0, 15)
reg_layout.addWidget(self.register_num)
layout.addLayout(reg_layout)
# Данные (только для записи)
self.data_label = QLabel("Данные (hex):")
self.data_edit = QLineEdit("0000")
self.data_edit.setMaximumWidth(100)
data_layout = QHBoxLayout()
data_layout.addWidget(self.data_label)
data_layout.addWidget(self.data_edit)
layout.addLayout(data_layout)
# Загружаем данные команды, если она передана
if self.command:
if self.command.get('type') == 'write':
self.cmd_type.setCurrentText("Запись")
self.device_addr.setValue(self.command.get('tetrad', 0))
self.register_num.setValue(self.command.get('register', 0))
self.data_edit.setText(f"{self.command.get('data', 0):04X}")
else:
self.cmd_type.setCurrentText("Чтение")
self.device_addr.setValue(self.command.get('tetrad', 0))
self.register_num.setValue(self.command.get('register', 0))
# Подключаем изменение типа команды
self.cmd_type.currentTextChanged.connect(self.on_cmd_type_changed)
self.on_cmd_type_changed(self.cmd_type.currentText())
# Кнопки
button_layout = QHBoxLayout()
self.ok_btn = QPushButton("OK")
self.ok_btn.clicked.connect(self._accept_command)
self.cancel_btn = QPushButton("Отмена")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.ok_btn)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
self.setLayout(layout)
def on_cmd_type_changed(self, text):
"""Показать/скрыть поле данных в зависимости от типа команды"""
if text == "Запись":
self.data_label.show()
self.data_edit.show()
else:
self.data_label.hide()
self.data_edit.hide()
def _accept_command(self):
if self.cmd_type.currentText() == "Запись":
try:
value = int(self.data_edit.text(), 16)
if not 0 <= value <= 0xFFFF:
raise ValueError()
except ValueError:
QMessageBox.warning(self, "Ошибка", "Данные должны быть в диапазоне 0000…FFFF")
return
self.accept()
def get_command(self):
"""Получить команду из диалога"""
cmd_type = 'write' if self.cmd_type.currentText() == "Запись" else 'read'
command = {
'type': cmd_type,
'tetrad': self.device_addr.value(),
'register': self.register_num.value()
}
if cmd_type == 'write':
try:
command['data'] = int(self.data_edit.text(), 16)
except ValueError:
command['data'] = 0
return command
class MacroEditorDialog(QDialog):
"""Диалог для создания/редактирования макроса"""
def __init__(self, parent=None, macro=None):
super().__init__(parent)
self.macro = deepcopy(macro) if macro else {'name': '', 'commands': []}
self.init_ui()
def init_ui(self):
self.setWindowTitle("Редактор макроса")
self.setMinimumSize(500, 400)
layout = QVBoxLayout()
# Имя макроса
name_layout = QHBoxLayout()
name_layout.addWidget(QLabel("Имя макроса:"))
self.macro_name = QLineEdit(self.macro.get('name', 'Новый макрос'))
name_layout.addWidget(self.macro_name)
layout.addLayout(name_layout)
# Список команд
layout.addWidget(QLabel("Команды:"))
self.commands_list = QListWidget()
layout.addWidget(self.commands_list)
# Кнопки управления командами
cmd_buttons_layout = QHBoxLayout()
self.add_cmd_btn = QPushButton("Добавить команду")
self.add_cmd_btn.clicked.connect(self.add_command)
cmd_buttons_layout.addWidget(self.add_cmd_btn)
self.edit_cmd_btn = QPushButton("Редактировать")
self.edit_cmd_btn.clicked.connect(self.edit_command)
cmd_buttons_layout.addWidget(self.edit_cmd_btn)
self.remove_cmd_btn = QPushButton("Удалить")
self.remove_cmd_btn.clicked.connect(self.remove_command)
cmd_buttons_layout.addWidget(self.remove_cmd_btn)
self.move_up_btn = QPushButton("Вверх")
self.move_up_btn.clicked.connect(self.move_command_up)
cmd_buttons_layout.addWidget(self.move_up_btn)
self.move_down_btn = QPushButton("Вниз")
self.move_down_btn.clicked.connect(self.move_command_down)
cmd_buttons_layout.addWidget(self.move_down_btn)
layout.addLayout(cmd_buttons_layout)
# Загружаем команды, если они есть
self.load_commands()
# Кнопки сохранения/отмены
button_layout = QHBoxLayout()
self.save_btn = QPushButton("Сохранить")
self.save_btn.clicked.connect(self.accept)
self.cancel_btn = QPushButton("Отмена")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
self.setLayout(layout)
def load_commands(self):
"""Загрузить команды в список"""
self.commands_list.clear()
for cmd in self.macro['commands']:
if cmd['type'] == 'write':
text = f"Запись: устр.{cmd['tetrad']}, рег.{cmd['register']}, 0x{cmd['data']:04X}"
else:
text = f"Чтение: устр.{cmd['tetrad']}, рег.{cmd['register']}"
self.commands_list.addItem(text)
def add_command(self):
"""Добавить новую команду"""
dialog = MacroCommandDialog(self)
if dialog.exec():
command = dialog.get_command()
self.macro['commands'].append(command)
self.load_commands()
def edit_command(self):
"""Редактировать выбранную команду"""
current_row = self.commands_list.currentRow()
if current_row >= 0 and current_row < len(self.macro['commands']):
dialog = MacroCommandDialog(self, self.macro['commands'][current_row])
if dialog.exec():
self.macro['commands'][current_row] = dialog.get_command()
self.load_commands()
def remove_command(self):
"""Удалить выбранную команду"""
current_row = self.commands_list.currentRow()
if current_row >= 0 and current_row < len(self.macro['commands']):
del self.macro['commands'][current_row]
self.load_commands()
def move_command_up(self):
"""Переместить команду вверх"""
current_row = self.commands_list.currentRow()
if current_row > 0:
self.macro['commands'][current_row], self.macro['commands'][current_row-1] = \
self.macro['commands'][current_row-1], self.macro['commands'][current_row]
self.load_commands()
self.commands_list.setCurrentRow(current_row-1)
def move_command_down(self):
"""Переместить команду вниз"""
current_row = self.commands_list.currentRow()
if current_row >= 0 and current_row < len(self.macro['commands']) - 1:
self.macro['commands'][current_row], self.macro['commands'][current_row+1] = \
self.macro['commands'][current_row+1], self.macro['commands'][current_row]
self.load_commands()
self.commands_list.setCurrentRow(current_row+1)
def get_macro(self):
"""Получить макрос из диалога"""
return {
'name': self.macro_name.text(),
'commands': self.macro['commands'].copy()
}
class SerialTab(QWidget):
"""Вкладка для работы с серийной шиной (ТОЛЬКО операции с шиной)"""
MACROS_FILE = "macros.json" # Файл для сохранения макросов
def __init__(self, serial_bus, raw_protocol, raw_widget, parent=None, *,
device_address=10, macros_file=None):
super().__init__(parent)
self.device_address = device_address
self.serial_bus = serial_bus
self.raw_protocol = raw_protocol
self.raw_widget = raw_widget
self.macros = [] # Список макросов
self.macro_buttons = [] # Список кнопок макросов
self._macro_running = False
self._macro_generation = 0
from pathlib import Path
self.MACROS_FILE = str(macros_file if macros_file is not None else
Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) / "pysetterminal" / "macros.json")
Path(self.MACROS_FILE).parent.mkdir(parents=True, exist_ok=True)
# Загружаем сохраненные макросы
self.load_macros()
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Группа для операций с шиной
bus_group = QGroupBox("Операции с шиной (через регистры 0x200A/0x200B/0x200F)")
bus_layout = QGridLayout()
bus_layout.addWidget(QLabel("Данные для шины (hex):"), 0, 0)
self.bus_data_edit = QLineEdit("0000")
self.bus_data_edit.setMaximumWidth(100)
bus_layout.addWidget(self.bus_data_edit, 0, 1)
bus_layout.addWidget(QLabel("Адреса (0-14):"), 0, 2)
self.bus_tetrad_edit = QLineEdit("0")
self.bus_tetrad_edit.setMaximumWidth(50)
bus_layout.addWidget(self.bus_tetrad_edit, 0, 3)
bus_layout.addWidget(QLabel("Регистр (0-15):"), 0, 4)
self.bus_register_edit = QLineEdit("0")
self.bus_register_edit.setMaximumWidth(50)
bus_layout.addWidget(self.bus_register_edit, 0, 5)
self.bus_write_btn = QPushButton("Записать в шину")
self.bus_write_btn.clicked.connect(self.write_to_bus)
self.bus_read_btn = QPushButton("Прочитать из шину")
self.bus_read_btn.clicked.connect(self.read_from_bus)
bus_buttons = QHBoxLayout()
bus_buttons.setSpacing(6)
bus_buttons.addWidget(self.bus_write_btn)
bus_buttons.addWidget(self.bus_read_btn)
bus_buttons.addStretch(1)
bus_layout.addLayout(bus_buttons, 1, 0, 1, 6)
bus_layout.setColumnStretch(6, 1)
bus_group.setLayout(bus_layout)
layout.addWidget(bus_group)
# --- Группа для макросов ---
macros_group = QGroupBox("Макросы")
macros_layout = QVBoxLayout()
# Кнопка создания нового макроса
self.create_macro_btn = QPushButton("Создать новый макрос")
self.create_macro_btn.clicked.connect(self.create_macro)
self.stop_macro_btn = QPushButton("Остановить макрос")
self.stop_macro_btn.setEnabled(False)
self.stop_macro_btn.clicked.connect(self.stop_macro)
macro_actions = QHBoxLayout()
macro_actions.setSpacing(6)
macro_actions.addWidget(self.create_macro_btn)
macro_actions.addWidget(self.stop_macro_btn)
macro_actions.addStretch(1)
macros_layout.addLayout(macro_actions)
for button in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, self.stop_macro_btn):
button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
button.setFixedHeight(30)
button.setStyleSheet("QPushButton { padding: 3px 10px; }")
self.macros_splitter = QSplitter(Qt.Orientation.Horizontal)
self.macros_splitter.setObjectName("macrosSplitter")
self.macros_splitter.setChildrenCollapsible(False)
self.macros_splitter.setHandleWidth(8)
self.macros_splitter.setToolTip("Перетащите разделитель, чтобы изменить ширину панелей")
self.macros_scroll_areas = []
self.macros_scroll_layouts = []
for side in ("Левая", "Правая"):
area = QScrollArea()
area.setAccessibleName(f"{side} панель макросов")
area.setWidgetResizable(True)
area.setMinimumSize(120, 120)
content = QWidget()
grid = QGridLayout(content)
grid.setAlignment(Qt.AlignTop | Qt.AlignLeft)
grid.setContentsMargins(6, 6, 6, 6)
grid.setVerticalSpacing(6)
area.setWidget(content)
self.macros_splitter.addWidget(area)
self.macros_scroll_areas.append(area)
self.macros_scroll_layouts.append(grid)
self.macros_splitter.setStretchFactor(0, 1)
self.macros_splitter.setStretchFactor(1, 1)
self.macros_splitter.setSizes([1, 1])
# Keep the existing attributes available to terminal integrations.
self.macros_scroll_area = self.macros_scroll_areas[0]
self.macros_scroll_content = self.macros_scroll_area.widget()
self.macros_scroll_layout = self.macros_scroll_layouts[0]
macros_layout.addWidget(self.macros_splitter, 1)
macros_group.setLayout(macros_layout)
layout.addWidget(macros_group, 1)
# --- ВНЕ ГРУППЫ: Результаты чтения ---
results_layout = QGridLayout()
# Считанное слово
results_layout.addWidget(QLabel("Считанное слово:"), 0, 0)
self.bus_read_value_label = QLabel("---")
self.bus_read_value_label.setStyleSheet("font-weight: bold; font-size: 14px; border: 1px solid #ccc; padding: 5px;")
results_layout.addWidget(self.bus_read_value_label, 0, 1)
# Ответ контроллера
results_layout.addWidget(QLabel("Ответ контроллера:"), 1, 0)
self.bus_result_label = QLabel("---")
self.bus_result_label.setStyleSheet("border: 1px solid #ccc; padding: 5px; font-family: monospace;")
self.bus_result_label.setWordWrap(True)
results_layout.addWidget(self.bus_result_label, 1, 1)
# Создаем фрейм для результатов
results_layout.setColumnStretch(2, 1)
self.bus_read_value_label.setMinimumWidth(180)
self.bus_result_label.setMaximumWidth(640)
results_frame = QFrame()
results_frame.setFrameShape(QFrame.StyledPanel)
results_frame.setLayout(results_layout)
layout.addWidget(results_frame)
self.setLayout(layout)
# После создания интерфейса создаем кнопки для загруженных макросов
# ВАЖНО: Используем enumerate для правильных индексов
for i, macro in enumerate(self.macros):
self.add_macro_button_with_index(macro, i)
def write_to_bus(self):
"""Запись в шину через регистры"""
try:
device_addr = self.device_address() if callable(self.device_address) else self.device_address
data = int(self.bus_data_edit.text(), 16)
tetrad = int(self.bus_tetrad_edit.text())
register_num = int(self.bus_register_edit.text())
success, result = self.serial_bus.write_bus(data, tetrad, register_num, device_addr)
if success:
self.raw_widget.append_text(f"Запись в шину: адрес={tetrad}, рег={register_num}, данные=0x{data:04X} - OK")
self.last_result = f"Запись в шину: данные=0x{data:04X}, адрес=0x{tetrad:X}, регистр=0x{register_num:X}"
self.last_result += f"\n{result}"
else:
self.raw_widget.append_text(f"Запись в шину: адрес={tetrad}, рег={register_num} - Ошибка: {result}")
self.last_result = f"Ошибка записи в шину: {result}"
except ValueError as e:
self.raw_widget.append_text(f"Ошибка в данных: {e}")
self.last_result = f"Ошибка в данных: {e}"
def read_from_bus(self):
"""Чтение из шины через регистры"""
try:
device_addr = self.device_address() if callable(self.device_address) else self.device_address
tetrad = int(self.bus_tetrad_edit.text())
register_num = int(self.bus_register_edit.text())
success, result = self.serial_bus.read_bus(tetrad, register_num, device_addr)
if success:
# result теперь словарь с данными
hex_str = ' '.join(f'{b:02X}' for b in result['raw_response']['raw'])
self.bus_result_label.setText(f"{hex_str}")
# Извлекаем считанное слово
read_word = result['value']
self.bus_read_value_label.setText(f"0x{read_word:04X} ({read_word})")
self.raw_widget.append_text(f"Чтение из шины: адрес={tetrad}, рег={register_num} = 0x{read_word:04X}")
self.last_result = f"Чтение из шины: адрес=0x{tetrad:X}, регистр=0x{register_num:X}"
self.last_result += f"\nСчитано: 0x{read_word:04X} ({read_word})"
self.last_result += f"\nОтвет контроллера: {hex_str}"
else:
self.bus_result_label.setText(f"Ошибка: {result}")
self.bus_read_value_label.setText("---")
self.raw_widget.append_text(f"Чтение из шины: адрес={tetrad}, рег={register_num} - Ошибка: {result}")
self.last_result = f"Ошибка чтения из шины: {result}"
except ValueError as e:
self.bus_result_label.setText(f"Ошибка: {e}")
self.bus_read_value_label.setText("---")
self.raw_widget.append_text(f"Ошибка в данных: {e}")
self.last_result = f"Ошибка в данных: {e}"
except ValueError as e:
self.bus_result_label.setText(f"Ошибка: {e}")
self.bus_read_value_label.setText("---")
error_text = f"Ошибка в данных: {e}"
self.raw_widget.append_text(f"✗ {error_text}")
self.last_result = error_text
def create_macro(self):
"""Создать новый макрос"""
dialog = MacroEditorDialog(self)
if dialog.exec():
macro = dialog.get_macro()
if not macro['name']:
macro['name'] = f"Макрос {len(self.macros) + 1}"
self.macros.append(macro)
self.add_macro_button(macro)
self.save_macros() # Сохраняем после добавления
dialog.deleteLater()
QApplication.processEvents() # Обрабатываем события
def add_macro_button_with_index(self, macro, index):
"""Добавить кнопку макроса в интерфейс с указанным индексом"""
items_per_row = 2
row = len(self.macro_buttons) // items_per_row
col = len(self.macro_buttons) % items_per_row
# Создаем контейнер для кнопки и кнопки редактирования
container = QWidget()
container_layout = QHBoxLayout()
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(4)
container.setMaximumWidth(360)
container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
# Основная кнопка макроса
macro_btn = QPushButton(macro['name'])
macro_btn.setFixedHeight(30)
macro_btn.setMinimumWidth(120)
macro_btn.setToolTip(macro["name"])
macro_btn.setStyleSheet("""
QPushButton {
font-weight: bold;
text-align: left;
padding: 3px 8px;
}
""")
# Используем partial для фиксации индекса
macro_btn.clicked.connect(partial(self.execute_macro_by_index, index))
# Кнопка редактирования
edit_btn = QPushButton()
edit_btn.setIcon(QIcon(_MacroActionIcon("edit", edit_btn)))
edit_btn.setIconSize(QSize(16, 16))
edit_btn.setFixedSize(30, 30)
edit_btn.setStyleSheet("QPushButton { padding: 0; }")
edit_btn.setAccessibleName("Редактировать макрос")
edit_btn.setToolTip("Редактировать макрос")
edit_btn.clicked.connect(partial(self.edit_macro_by_index, index))
# Кнопка удаления
delete_btn = QPushButton()
delete_btn.setIcon(QIcon(_MacroActionIcon("delete", delete_btn)))
delete_btn.setIconSize(QSize(16, 16))
delete_btn.setFixedSize(30, 30)
delete_btn.setStyleSheet("QPushButton { padding: 0; }")
delete_btn.setAccessibleName("Удалить макрос")
delete_btn.setToolTip("Удалить макрос")
delete_btn.clicked.connect(partial(self.delete_macro, index))
container_layout.addWidget(macro_btn)
container_layout.addWidget(edit_btn)
container_layout.addWidget(delete_btn)
container.setLayout(container_layout)
self.macros_scroll_layouts[col].addWidget(container, row, 0)
self.macro_buttons.append(container)
def add_macro_button(self, macro):
"""Добавить кнопку макроса в интерфейс (при создании нового)"""
# При создании нового макроса используем последний индекс
index = len(self.macros) - 1
self.add_macro_button_with_index(macro, index)
def execute_macro_by_index(self, macro_index):
"""Выполнить макрос по индексу"""
if 0 <= macro_index < len(self.macros):
self.execute_macro_commands(self.macros[macro_index]['commands'])
def edit_macro_by_index(self, macro_index):
"""Редактировать макрос по индексу"""
if 0 <= macro_index < len(self.macros):
dialog = MacroEditorDialog(self, self.macros[macro_index])
if dialog.exec():
edited_macro = dialog.get_macro()
self.macros[macro_index] = edited_macro
# Обновляем кнопку - находим ее по индексу
if macro_index < len(self.macro_buttons):
container = self.macro_buttons[macro_index]
macro_btn = container.layout().itemAt(0).widget()
if macro_btn:
macro_btn.setText(edited_macro['name'])
self.save_macros()
dialog.deleteLater()
QApplication.processEvents() # Обрабатываем события
# Добавим метод для удаления макроса
def delete_macro(self, macro_index):
"""Удалить макрос"""
if 0 <= macro_index < len(self.macros):
# Спросим подтверждение
reply = QMessageBox.question(
self,
"Удаление макроса",
f"Удалить макрос '{self.macros[macro_index]['name']}'?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# Удаляем макрос из списка
del self.macros[macro_index]
# Полностью перестраиваем layout
self.rebuild_macros_layout()
# Сохраняем изменения
self.save_macros()
def execute_macro_commands(self, commands):
"""Run a macro as Qt callbacks, without loops or waiting in the UI thread."""
if self._macro_running:
self.raw_widget.append_text("Дождитесь завершения макроса")
return
from protocan.tms_terminal import bus_steps
try:
controller = self.device_address() if callable(self.device_address) else self.device_address
jobs = []
for command in commands:
if command['type'] not in ('write', 'read'):
raise ValueError("Неизвестная команда макроса")
jobs.extend(bus_steps(command['type'] == 'write', command['tetrad'],
command['register'], command.get('data', 0)))
except (KeyError, ValueError, RuntimeError, TypeError) as error:
self.raw_widget.append_text(f"Макрос остановлен: {error}")
return
if not jobs:
self.raw_widget.append_text("Макрос пуст")
return
self._macro_controller = controller
self._macro_jobs = jobs
self._macro_index = 0
self._macro_running = True
self._macro_generation += 1
self._set_macro_controls(True)
generation = self._macro_generation
QTimer.singleShot(0, lambda: self._next_macro_command(generation))
def _set_macro_controls(self, running):
self.stop_macro_btn.setEnabled(running)
for widget in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn,
self.macros_splitter, self.raw_widget):
widget.setEnabled(not running)
def _next_macro_command(self, generation):
if not self._macro_running or generation != self._macro_generation:
return
if self._macro_index >= len(self._macro_jobs):
self._finish_macro("Макрос выполнен")
return
generation = self._macro_generation
address, value = self._macro_jobs[self._macro_index]
self.raw_protocol.request_async(address, value, self._macro_controller,
lambda success, result: self._macro_reply(generation, success, result))
def _macro_reply(self, generation, success, result):
if not self._macro_running or generation != self._macro_generation:
return
if not success:
self._finish_macro(f"Макрос остановлен: {result}")
return
if 'value' in result:
value = result['value']
self.bus_read_value_label.setText(f"0x{value:04X} ({value})")
self.bus_result_label.setText(result['raw_response']['raw'].hex(" "))
self.raw_widget.append_text(f"Макрос: прочитано 0x{value:04X}")
else:
self.raw_widget.append_text("Макрос: запись подтверждена")
self._macro_index += 1
QTimer.singleShot(0, lambda: self._next_macro_command(generation))
def _finish_macro(self, message):
self._macro_running = False
self._set_macro_controls(False)
self.last_result = message
self.raw_widget.append_text(message)
def stop_macro(self):
if not self._macro_running:
return
self._macro_generation += 1
self._finish_macro("Макрос остановлен пользователем; переданные команды могли быть выполнены")
self.raw_protocol.cancel_async()
def load_macros(self):
"""Загрузить макросы из файла"""
if os.path.exists(self.MACROS_FILE):
try:
with open(self.MACROS_FILE, 'r', encoding='utf-8') as f:
self.macros = json.load(f)
print(f"Загружено {len(self.macros)} макросов")
except Exception as e:
print(f"Ошибка загрузки макросов: {e}")
self.macros = []
else:
self.macros = []
def save_macros(self):
"""Сохранить макросы в файл"""
try:
with open(self.MACROS_FILE, 'w', encoding='utf-8') as f:
json.dump(self.macros, f, ensure_ascii=False, indent=2)
print(f"Сохранено {len(self.macros)} макросов")
except Exception as e:
print(f"Ошибка сохранения макросов: {e}")
def rebuild_macros_layout(self):
"""Перестроить layout макросов"""
# Очищаем layout
for grid in self.macros_scroll_layouts:
while grid.count():
item = grid.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Очищаем список кнопок
self.macro_buttons.clear()
# Пересоздаем все кнопки с правильными индексами
for i, macro in enumerate(self.macros):
self.add_macro_button_with_index(macro, i)
def get_last_result(self):
"""Получить последний результат операции"""
result = getattr(self, 'last_result', '')
return result