init commit

Работа с сериальной шиной и макросами
This commit is contained in:
2026-02-10 11:32:38 +03:00
commit c287274588
5 changed files with 1341 additions and 0 deletions

659
serialbus.py Normal file
View File

@@ -0,0 +1,659 @@
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