Files
PySetTerminal/serialbus.py
Razvalyaev c287274588 init commit
Работа с сериальной шиной и макросами
2026-02-10 11:32:38 +03:00

659 lines
30 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
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