import json import os from functools import partial from PySide2.QtWidgets import * from PySide2.QtCore import * from PySide2.QtGui import * class SerialBus: """ВЫСОКОУРОВНЕВЫЙ протокол для работы с серийной шиной (только через регистры)""" def __init__(self, raw_protocol): self.raw = raw_protocol # Используем низкоуровневый протокол def write_bus(self, data_word, address_tetrad, register_num=0, device_address=1): """ Запись в шину по протоколу: 1. POKE 0x200A data_word 2. POKE 0x200B (0x80AR | (address_tetrad << 4)), где R - номер регистра """ # 1. Запись данных в регистр шины success1, result1 = self.raw.poke(0x200A, data_word, device_address) if not success1: return False, f"Ошибка записи данных: {result1}" # 2. Запись управляющего слова control_word = 0x8000 | ((address_tetrad & 0xF) << 4) | (register_num & 0xF) success2, result2 = self.raw.poke(0x200B, control_word, device_address) if success2: return True, f"Запись успешна: управляющее слово=0x{control_word:04X}" else: return False, f"Ошибка записи управляющего слова: {result2}" def read_bus(self, address_tetrad, register_num=0, device_address=1): """ Чтение из шины по протоколу: 1. POKE 0x200B (0x00AR | (address_tetrad << 4)), где R - номер регистра 2. PEEK 0x200F """ # 1. Запись управляющего слова для чтения control_word = 0x0000 | ((address_tetrad & 0xF) << 4) | (register_num & 0xF) success1, result1 = self.raw.poke(0x200B, control_word, device_address) if not success1: return False, f"Ошибка записи управляющего слова: {result1}" # 2. Чтение данных из регистра шины success2, result2 = self.raw.peek(0x200F, device_address) if success2: return True, result2 else: return False, result2 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) 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 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 = 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): super().__init__() self.serial_bus = serial_bus self.raw_protocol = raw_protocol self.raw_widget = raw_widget self.macros = [] # Список макросов self.macro_buttons = [] # Список кнопок макросов # Загружаем сохраненные макросы 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) bus_layout.addWidget(self.bus_write_btn, 1, 0, 1, 3) self.bus_read_btn = QPushButton("Прочитать из шину") self.bus_read_btn.clicked.connect(self.read_from_bus) bus_layout.addWidget(self.bus_read_btn, 1, 3, 1, 3) 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) macros_layout.addWidget(self.create_macro_btn) # Прокручиваемая область для кнопок макросов self.macros_scroll_area = QScrollArea() self.macros_scroll_area.setWidgetResizable(True) # Устанавливаем фиксированную высоту, чтобы вместить 3-4 строки макросов self.macros_scroll_area.setMinimumHeight(150) # Высота для 3-4 строк self.macros_scroll_content = QWidget() self.macros_scroll_layout = QGridLayout() self.macros_scroll_content.setLayout(self.macros_scroll_layout) self.macros_scroll_area.setWidget(self.macros_scroll_content) self.macros_scroll_layout.setAlignment(Qt.AlignTop) macros_layout.addWidget(self.macros_scroll_area) macros_group.setLayout(macros_layout) layout.addWidget(macros_group) # --- ВНЕ ГРУППЫ: Результаты чтения --- 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_frame = QFrame() results_frame.setFrameShape(QFrame.StyledPanel) results_frame.setLayout(results_layout) layout.addWidget(results_frame) layout.addStretch() 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 = 10 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 = 10 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): """Добавить кнопку макроса в интерфейс с указанным индексом""" # Изменяем расчет строк и колонок для 3 колонок items_per_row = 3 # Теперь 3 кнопки в строке 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) # Основная кнопка макроса macro_btn = QPushButton(macro['name']) macro_btn.setMinimumHeight(40) macro_btn.setStyleSheet(""" QPushButton { font-weight: bold; text-align: left; padding: 10px; } QPushButton:hover { background-color: #e0e0e0; } """) # Используем partial для фиксации индекса macro_btn.clicked.connect(partial(self.execute_macro_by_index, index)) # Кнопка редактирования edit_btn = QPushButton("✎") edit_btn.setFixedSize(30, 30) edit_btn.setToolTip("Редактировать макрос") edit_btn.clicked.connect(partial(self.edit_macro_by_index, index)) # Кнопка удаления delete_btn = QPushButton("✕") delete_btn.setFixedSize(30, 30) 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_layout.addWidget(container, row, col, 1, 1) # span изменен на 1 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): """Выполнить список команд макроса""" last_read_result = None for i, cmd in enumerate(commands): try: if cmd['type'] == 'write': success, result = self.serial_bus.write_bus( cmd['data'], cmd['tetrad'], cmd['register'], 10 ) if success: self.raw_widget.append_text(f"Макрос: запись устр.{cmd['tetrad']}, рег.{cmd['register']}, 0x{cmd['data']:04X} - OK") else: self.raw_widget.append_text(f"Макрос: запись устр.{cmd['tetrad']}, рег.{cmd['register']} - Ошибка: {result}") else: # read success, result = self.serial_bus.read_bus( cmd['tetrad'], cmd['register'], 10 ) if success: # Сохраняем последний результат чтения для отображения last_read_result = result hex_str = ' '.join(f'{b:02X}' for b in result['raw_response']['raw']) read_word = result['value'] self.raw_widget.append_text(f"Макрос: чтение устр.{cmd['tetrad']}, рег.{cmd['register']} = 0x{read_word:04X}") else: self.raw_widget.append_text(f"Макрос: чтение устр.{cmd['tetrad']}, рег.{cmd['register']} - Ошибка: {result}") except Exception as e: self.raw_widget.append_text(f"Макрос команда {i+1}: ошибка - {e}") # Обновляем интерфейс с результатами последнего чтения if last_read_result: hex_str = ' '.join(f'{b:02X}' for b in last_read_result['raw_response']['raw']) read_word = last_read_result['value'] self.bus_result_label.setText(hex_str) self.bus_read_value_label.setText(f"0x{read_word:04X} ({read_word})") 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 i in reversed(range(self.macros_scroll_layout.count())): item = self.macros_scroll_layout.itemAt(i) if item and item.widget(): item.widget().setParent(None) # Очищаем список кнопок 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