init commit
Работа с сериальной шиной и макросами
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
/.vscode
|
||||
/__pycache__
|
||||
/macros.json
|
||||
/BusTerminal.exe
|
||||
124
build_and_clean.py
Normal file
124
build_and_clean.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
from pathlib import Path
|
||||
import PySide2
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
# install: pip install PySide2 lxml nuitka pyinstaller
|
||||
# - PyInstaller
|
||||
# - nuitka
|
||||
# - PySide2
|
||||
# - clang
|
||||
|
||||
# === Конфигурация ===
|
||||
USE_NUITKA = True # True — сборка через Nuitka, False — через PyInstaller
|
||||
MAIN_SCRIPT_NAME = "mainterm"
|
||||
OUTPUT_NAME = "BusTerminal"
|
||||
USE_ICON = False # True — использовать иконку, False — без иконки
|
||||
|
||||
|
||||
SRC_PATH = Path("./")
|
||||
SCRIPT_PATH = SRC_PATH / (MAIN_SCRIPT_NAME + ".py")
|
||||
|
||||
DIST_PATH = Path("./").resolve()
|
||||
WORK_PATH = Path("./build_temp").resolve()
|
||||
SPEC_PATH = WORK_PATH
|
||||
ICON_PATH = SRC_PATH / "icon.png"
|
||||
ICON_ICO_PATH = SRC_PATH / "icon.ico"
|
||||
|
||||
TEMP_FOLDERS = [
|
||||
"build_temp",
|
||||
"__pycache__",
|
||||
MAIN_SCRIPT_NAME + ".build",
|
||||
MAIN_SCRIPT_NAME + ".onefile-build",
|
||||
MAIN_SCRIPT_NAME + ".dist"
|
||||
]
|
||||
# === Пути к DLL и прочим зависимостям ===
|
||||
LIBS = {
|
||||
}
|
||||
|
||||
# === PySide2 плагины ===
|
||||
PySide2_path = Path(PySide2.__file__).parent
|
||||
datas = []
|
||||
datas += collect_data_files('PySide2', includes=['plugins/platforms/*'])
|
||||
datas += collect_data_files('PySide2', includes=['plugins/styles/*'])
|
||||
datas += collect_data_files('PySide2', includes=['plugins/imageformats/*'])
|
||||
|
||||
add_data_list = [f"{src};{dest}" for src, dest in datas]
|
||||
|
||||
# Проверка наличия DLL и добавление
|
||||
add_binary_list = []
|
||||
for name, path in LIBS.items():
|
||||
if path.exists():
|
||||
add_binary_list.append(f"{str(path)};{name}")
|
||||
else:
|
||||
print(f"WARNING: {path.name} не найден — он не будет включён в сборку")
|
||||
|
||||
def clean_temp():
|
||||
for folder in TEMP_FOLDERS:
|
||||
path = Path(folder)
|
||||
if path.exists():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
if USE_NUITKA:
|
||||
# Формируем include-data-file только для DLL
|
||||
include_data_files = [f"--include-data-file={str(path)}={name}" for name, path in LIBS.items() if path.exists()]
|
||||
|
||||
# Добавляем опциональную иконку как встроенный ресурс
|
||||
if USE_ICON and ICON_ICO_PATH.exists():
|
||||
include_data_files.append(f"--include-data-file={ICON_ICO_PATH}=icon.ico")
|
||||
|
||||
cmd = [
|
||||
"python", "-m", "nuitka",
|
||||
"--standalone",
|
||||
"--onefile",
|
||||
"--enable-plugin=pyside2",
|
||||
"--windows-console-mode=disable",
|
||||
f"--output-dir={DIST_PATH}",
|
||||
f"--output-filename={OUTPUT_NAME}.exe",
|
||||
]
|
||||
|
||||
# Добавляем параметр иконки только если она нужна и существует
|
||||
if USE_ICON and ICON_ICO_PATH.exists():
|
||||
cmd.append(f"--windows-icon-from-ico={ICON_ICO_PATH}")
|
||||
|
||||
cmd.extend(include_data_files)
|
||||
cmd.append(str(SCRIPT_PATH))
|
||||
|
||||
else:
|
||||
# PyInstaller
|
||||
cmd = [
|
||||
"pyinstaller",
|
||||
"--name", OUTPUT_NAME,
|
||||
"--distpath", str(DIST_PATH),
|
||||
"--workpath", str(WORK_PATH),
|
||||
"--specpath", str(SPEC_PATH),
|
||||
"--windowed",
|
||||
"--hidden-import=PySide2.QtWidgets",
|
||||
"--hidden-import=PySide2.QtGui",
|
||||
"--hidden-import=PySide2.QtCore",
|
||||
*[arg for b in add_binary_list for arg in ("--add-binary", b)],
|
||||
*[arg for d in add_data_list for arg in ("--add-data", d)],
|
||||
]
|
||||
|
||||
# Добавляем иконку только если она нужна и существует
|
||||
if USE_ICON and ICON_ICO_PATH.exists():
|
||||
cmd.append(f"--icon={ICON_ICO_PATH}")
|
||||
else:
|
||||
print("WARNING: Иконка не используется или не найдена")
|
||||
|
||||
cmd.append(str(SCRIPT_PATH))
|
||||
|
||||
# === Запуск сборки ===
|
||||
print("Выполняется сборка с помощью " + ("Nuitka" if USE_NUITKA else "PyInstaller"))
|
||||
print(" ".join(cmd))
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
print("\nСборка успешно завершена!")
|
||||
|
||||
# Удаление временных папок после сборки
|
||||
clean_temp()
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
print("\nОшибка при сборке.")
|
||||
203
mainterm.py
Normal file
203
mainterm.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import sys
|
||||
import serial.tools.list_ports
|
||||
from PySide2.QtWidgets import *
|
||||
from PySide2.QtCore import *
|
||||
from PySide2.QtGui import *
|
||||
|
||||
from rawprotocol import RawProtocol, RawProtocolWidget
|
||||
from serialbus import SerialBus, SerialTab
|
||||
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Главное окно приложения"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Создаем низкоуровневый протокол
|
||||
self.raw_protocol = RawProtocol()
|
||||
# Создаем высокоуровневый протокол, передавая ему low-level
|
||||
self.serial_bus = SerialBus(self.raw_protocol)
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("Терминал")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
|
||||
# Главный виджет и компоновка
|
||||
main_widget = QWidget()
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
# Центральный виджет с вкладками
|
||||
self.tab_widget = QTabWidget()
|
||||
|
||||
# Виджет для RAW протокола (всегда внизу)
|
||||
self.raw_widget = RawProtocolWidget(self.raw_protocol)
|
||||
|
||||
# Вкладка для серийной шины
|
||||
self.serial_tab = SerialTab(self.serial_bus, self.raw_protocol, self.raw_widget)
|
||||
self.tab_widget.addTab(self.serial_tab, "Серийная шина")
|
||||
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
main_layout.addWidget(self.raw_widget)
|
||||
|
||||
main_widget.setLayout(main_layout)
|
||||
self.setCentralWidget(main_widget)
|
||||
|
||||
# Создание меню
|
||||
self.create_menu()
|
||||
|
||||
# Создание панели инструментов
|
||||
self.create_toolbar()
|
||||
|
||||
def create_menu(self):
|
||||
menubar = self.menuBar()
|
||||
|
||||
# Меню Файл
|
||||
file_menu = menubar.addMenu("Файл")
|
||||
|
||||
exit_action = QAction("Выход", self)
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
# Меню Помощь
|
||||
help_menu = menubar.addMenu("Помощь")
|
||||
|
||||
protocol_action = QAction("Информация о протоколе", self)
|
||||
protocol_action.triggered.connect(self.show_protocol_info)
|
||||
help_menu.addAction(protocol_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
toolbar = self.addToolBar("Подключение")
|
||||
|
||||
# Выбор COM порта
|
||||
toolbar.addWidget(QLabel("COM порт:"))
|
||||
|
||||
self.port_combo = QComboBox()
|
||||
self.port_combo.setMinimumWidth(150)
|
||||
self.refresh_ports()
|
||||
toolbar.addWidget(self.port_combo)
|
||||
|
||||
# Кнопка обновления портов
|
||||
refresh_btn = QToolButton()
|
||||
refresh_btn.setText("⟳")
|
||||
refresh_btn.setToolTip("Обновить список портов")
|
||||
refresh_btn.clicked.connect(self.refresh_ports)
|
||||
toolbar.addWidget(refresh_btn)
|
||||
|
||||
# Кнопка подключения/отключения
|
||||
self.connect_btn = QPushButton("Подключить")
|
||||
self.connect_btn.clicked.connect(self.toggle_connection)
|
||||
toolbar.addWidget(self.connect_btn)
|
||||
|
||||
# Индикатор статуса
|
||||
self.status_label = QLabel("Отключено")
|
||||
self.status_label.setStyleSheet("color: red;")
|
||||
toolbar.addWidget(self.status_label)
|
||||
|
||||
toolbar.addSeparator()
|
||||
|
||||
# Настройки скорости
|
||||
toolbar.addWidget(QLabel("Скорость:"))
|
||||
self.baud_combo = QComboBox()
|
||||
self.baud_combo.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"])
|
||||
self.baud_combo.setCurrentText("115200")
|
||||
toolbar.addWidget(self.baud_combo)
|
||||
|
||||
def refresh_ports(self):
|
||||
"""Обновление списка доступных COM портов"""
|
||||
self.port_combo.clear()
|
||||
ports = serial.tools.list_ports.comports()
|
||||
for port in ports:
|
||||
self.port_combo.addItem(f"{port.device} - {port.description}")
|
||||
|
||||
if not ports:
|
||||
self.port_combo.addItem("Порты не найдены")
|
||||
|
||||
def toggle_connection(self):
|
||||
"""Подключение/отключение от COM порта"""
|
||||
if self.raw_protocol.is_connected:
|
||||
# Отключение
|
||||
self.raw_protocol.disconnect()
|
||||
self.connect_btn.setText("Подключить")
|
||||
self.status_label.setText("Отключено")
|
||||
self.status_label.setStyleSheet("color: red;")
|
||||
|
||||
# Блокировка элементов управления
|
||||
self.serial_tab.bus_write_btn.setEnabled(False)
|
||||
self.serial_tab.bus_read_btn.setEnabled(False)
|
||||
self.raw_widget.raw_send_btn.setEnabled(False)
|
||||
self.raw_widget.raw_read_btn.setEnabled(False)
|
||||
else:
|
||||
# Подключение
|
||||
port_text = self.port_combo.currentText()
|
||||
if " - " in port_text:
|
||||
port_name = port_text.split(" - ")[0]
|
||||
else:
|
||||
port_name = port_text
|
||||
|
||||
try:
|
||||
baudrate = int(self.baud_combo.currentText())
|
||||
if self.raw_protocol.connect(port_name, baudrate):
|
||||
self.connect_btn.setText("Отключить")
|
||||
self.status_label.setText("Подключено")
|
||||
self.status_label.setStyleSheet("color: green;")
|
||||
|
||||
# Разблокировка элементов управления
|
||||
self.serial_tab.bus_write_btn.setEnabled(True)
|
||||
self.serial_tab.bus_read_btn.setEnabled(True)
|
||||
self.raw_widget.raw_send_btn.setEnabled(True)
|
||||
self.raw_widget.raw_read_btn.setEnabled(True)
|
||||
else:
|
||||
QMessageBox.warning(self, "Ошибка", "Не удалось подключиться к порту")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Ошибка подключения: {e}")
|
||||
|
||||
def show_protocol_info(self):
|
||||
"""Показать информацию о протоколе"""
|
||||
info = """
|
||||
ИНФОРМАЦИЯ О ПРОТОКОЛЕ:
|
||||
|
||||
На основе кода контроллера:
|
||||
|
||||
1. ФОРМАТ СООБЩЕНИЙ:
|
||||
[адрес][команда][данные][CRC16-IBM]
|
||||
|
||||
2. КЛЮЧЕВЫЕ АДРЕСА:
|
||||
- 0x200A: Регистр данных шины (запись)
|
||||
- 0x200B: Регистр управления шиной
|
||||
- 0x200F: Регистр данных шины (чтение)
|
||||
|
||||
3. ПРОТОКОЛ ШИНЫ:
|
||||
ЗАПИСЬ:
|
||||
1. POKE 0x200A <данные>
|
||||
2. POKE 0x200B 0x80A0 | (адрес << 4)
|
||||
|
||||
ЧТЕНИЕ:
|
||||
1. POKE 0x200B 0x00A0 | (адрес << 4)
|
||||
2. PEEK 0x200F
|
||||
|
||||
4. ЧТО НУЖНО УТОЧНИТЬ:
|
||||
- Точные коды команд CMD_RS232_PEEK и CMD_RS232_POKE
|
||||
- Длину полей в сообщениях
|
||||
- Формат ответов от контроллера
|
||||
"""
|
||||
|
||||
QMessageBox.information(self, "Информация о протоколе", info)
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Установка стиля
|
||||
app.setStyle("Fusion")
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
350
rawprotocol.py
Normal file
350
rawprotocol.py
Normal file
@@ -0,0 +1,350 @@
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import time
|
||||
from PySide2.QtWidgets import *
|
||||
from PySide2.QtCore import *
|
||||
from PySide2.QtGui import *
|
||||
|
||||
|
||||
class RawProtocol:
|
||||
"""НИЗКОУРОВНЕВЫЙ ПРОТОКОЛ PEEK/POKE с полной реализацией протокола контроллера"""
|
||||
|
||||
def __init__(self):
|
||||
self.serial_port = None
|
||||
self.is_connected = False
|
||||
|
||||
# Коды команд из кода контроллера
|
||||
self.CMD_PEEK = 56 # CMD_RS232_PEEK
|
||||
self.CMD_POKE = 57 # CMD_RS232_POKE
|
||||
|
||||
def connect(self, port_name, baudrate=115200):
|
||||
"""Подключение к COM порту"""
|
||||
try:
|
||||
self.serial_port = serial.Serial(
|
||||
port=port_name,
|
||||
baudrate=baudrate,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=1
|
||||
)
|
||||
self.is_connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Ошибка подключения: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Отключение от COM порта"""
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
self.serial_port.close()
|
||||
self.is_connected = False
|
||||
|
||||
def calculate_crc(self, data, init=0xFFFF):
|
||||
"""CRC16-IBM (aka CRC-16/ANSI, polynomial 0xA001 reflected)."""
|
||||
crc = init
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
def send_command(self, address, command, data_bytes):
|
||||
"""Отправка команды по протоколу контроллера"""
|
||||
if not self.is_connected:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Формируем сообщение без CRC
|
||||
message = bytearray()
|
||||
message.append(address) # Адрес устройства
|
||||
message.append(command) # Код команды
|
||||
message.extend(data_bytes) # Данные команды
|
||||
|
||||
# Добавляем CRC
|
||||
crc = self.calculate_crc(message)
|
||||
message.extend(crc.to_bytes(2, 'little'))
|
||||
|
||||
# Отправляем
|
||||
self.serial_port.write(message)
|
||||
self.serial_port.flush()
|
||||
|
||||
# Формируем hex строку с пробелами между байтами
|
||||
hex_str = ' '.join(f'{b:02X}' for b in message)
|
||||
print(f"Отправлено: {hex_str}")
|
||||
|
||||
return message
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка отправки команды: {e}")
|
||||
return None
|
||||
|
||||
def validate_response(self, response_bytes):
|
||||
"""Проверка валидности ответа от контроллера"""
|
||||
if len(response_bytes) < 4: # минимум адрес+команда+CRC
|
||||
return False, "Слишком короткий ответ"
|
||||
|
||||
# Проверяем CRC
|
||||
received_crc = int.from_bytes(response_bytes[-2:], 'little')
|
||||
calculated_crc = self.calculate_crc(response_bytes[:-2])
|
||||
|
||||
if received_crc != calculated_crc:
|
||||
return False, f"Ошибка CRC: получено 0x{received_crc:04X}, ожидалось 0x{calculated_crc:04X}"
|
||||
|
||||
# Извлекаем данные из ответа
|
||||
address = response_bytes[0]
|
||||
command = response_bytes[1]
|
||||
data = response_bytes[2:-2]
|
||||
|
||||
return True, {
|
||||
'address': address,
|
||||
'command': command,
|
||||
'data': data,
|
||||
'raw': response_bytes
|
||||
}
|
||||
|
||||
def receive_response(self, timeout=1.0):
|
||||
"""Прием ответа от контроллера"""
|
||||
if not self.is_connected:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Читаем данные с таймаутом
|
||||
self.serial_port.timeout = timeout
|
||||
response = self.serial_port.read(1024)
|
||||
|
||||
if response:
|
||||
# Формируем hex строку с пробелами между байтами
|
||||
hex_str = ' '.join(f'{b:02X}' for b in response)
|
||||
print(f"Получено: {hex_str}")
|
||||
return response
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка приема: {e}")
|
||||
return None
|
||||
|
||||
def send_and_receive(self, address, command, data_bytes, timeout=1.0):
|
||||
"""Отправить команду и получить ответ от TMS"""
|
||||
# Отправляем команду
|
||||
sent = self.send_command(address, command, data_bytes)
|
||||
if not sent:
|
||||
return False, "Не удалось отправить команду"
|
||||
|
||||
# Ждем ответ
|
||||
time.sleep(0.01) # небольшая пауза
|
||||
response = self.receive_response(timeout)
|
||||
if not response:
|
||||
return False, "Нет ответа от устройства"
|
||||
|
||||
# Проверяем ответ
|
||||
return self.validate_response(response)
|
||||
|
||||
def poke(self, mem_address, data_value, device_address=1):
|
||||
"""ПОЛНАЯ команда POKE - запись в память с CRC и анализом ответа"""
|
||||
# Формируем данные команды POKE
|
||||
data = bytearray()
|
||||
data.extend(mem_address.to_bytes(4, 'little'))
|
||||
data.extend(data_value.to_bytes(4, 'little'))
|
||||
|
||||
# Отправляем и получаем ответ
|
||||
success, result = self.send_and_receive(device_address, self.CMD_POKE, data)
|
||||
|
||||
if success:
|
||||
# Ответ успешен, проверяем что это ответ на POKE
|
||||
response_data = result['data']
|
||||
if len(response_data) >= 1:
|
||||
# В ответе POKE обычно есть подтверждение
|
||||
return True, result
|
||||
return False, result
|
||||
|
||||
def peek(self, mem_address, device_address=1):
|
||||
"""ПОЛНАЯ команда PEEK - чтение из памяти с CRC и анализом ответа"""
|
||||
# Формируем данные команды PEEK
|
||||
data = bytearray()
|
||||
data.extend(mem_address.to_bytes(4, 'little'))
|
||||
|
||||
# Отправляем и получаем ответ
|
||||
success, result = self.send_and_receive(device_address, self.CMD_PEEK, data)
|
||||
|
||||
if success:
|
||||
# Извлекаем данные из ответа PEEK
|
||||
response_data = result['data']
|
||||
if len(response_data) >= 2:
|
||||
# Предполагаем что данные идут первыми 2 байтами
|
||||
read_value = int.from_bytes(response_data[:2], 'little')
|
||||
return True, {'value': read_value, 'raw_response': result}
|
||||
return False, result
|
||||
|
||||
def send_raw(self, hex_data):
|
||||
"""Отправка сырых данных (для отладки)"""
|
||||
try:
|
||||
hex_str = hex_data.replace(" ", "")
|
||||
data = bytes.fromhex(hex_str)
|
||||
|
||||
if self.is_connected and self.serial_port:
|
||||
self.serial_port.write(data)
|
||||
self.serial_port.flush()
|
||||
return True, f"Отправлено: {hex_str}"
|
||||
else:
|
||||
return False, "Порт не подключен"
|
||||
|
||||
except ValueError:
|
||||
return False, "Некорректные hex данные"
|
||||
|
||||
def receive_raw(self, max_bytes=1024):
|
||||
"""Чтение сырых данных (для отладки)"""
|
||||
if self.is_connected and self.serial_port:
|
||||
data = self.serial_port.read(max_bytes)
|
||||
if data:
|
||||
return True, data.hex()
|
||||
else:
|
||||
return False, "Нет данных"
|
||||
else:
|
||||
return False, "Порт не подключен"
|
||||
|
||||
|
||||
|
||||
class RawProtocolWidget(QWidget):
|
||||
"""Виджет для отладки RAW протокола (всегда виден внизу окна)"""
|
||||
|
||||
def __init__(self, raw_protocol):
|
||||
super().__init__()
|
||||
self.raw_protocol = raw_protocol
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE)
|
||||
mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)")
|
||||
mem_layout = QGridLayout()
|
||||
|
||||
mem_layout.addWidget(QLabel("Адрес памяти (hex):"), 0, 0)
|
||||
self.mem_addr_edit = QLineEdit("2000")
|
||||
self.mem_addr_edit.setMaximumWidth(100)
|
||||
mem_layout.addWidget(self.mem_addr_edit, 0, 1)
|
||||
|
||||
mem_layout.addWidget(QLabel("Данные (hex):"), 1, 0)
|
||||
self.mem_data_edit = QLineEdit("0000")
|
||||
self.mem_data_edit.setMaximumWidth(100)
|
||||
mem_layout.addWidget(self.mem_data_edit, 1, 1)
|
||||
|
||||
self.mem_write_btn = QPushButton("Записать (POKE)")
|
||||
self.mem_write_btn.clicked.connect(self.memory_write)
|
||||
mem_layout.addWidget(self.mem_write_btn, 0, 2)
|
||||
|
||||
self.mem_read_btn = QPushButton("Прочитать (PEEK)")
|
||||
self.mem_read_btn.clicked.connect(self.memory_read)
|
||||
mem_layout.addWidget(self.mem_read_btn, 1, 2)
|
||||
|
||||
self.mem_result_label = QLabel("---")
|
||||
mem_layout.addWidget(self.mem_result_label, 1, 3)
|
||||
|
||||
mem_group.setLayout(mem_layout)
|
||||
layout.addWidget(mem_group)
|
||||
|
||||
# Группа для RAW отправки
|
||||
raw_group = QGroupBox("RAW отправка/чтение")
|
||||
raw_layout = QVBoxLayout()
|
||||
|
||||
self.raw_send_edit = QLineEdit()
|
||||
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
|
||||
raw_layout.addWidget(self.raw_send_edit)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
self.raw_send_btn = QPushButton("Отправить RAW")
|
||||
self.raw_send_btn.clicked.connect(self.send_raw_data)
|
||||
hbox.addWidget(self.raw_send_btn)
|
||||
|
||||
self.raw_read_btn = QPushButton("Прочитать RAW")
|
||||
self.raw_read_btn.clicked.connect(self.read_raw_data)
|
||||
hbox.addWidget(self.raw_read_btn)
|
||||
|
||||
|
||||
raw_layout.addLayout(hbox)
|
||||
|
||||
self.raw_output = QTextEdit()
|
||||
self.raw_output.setReadOnly(True)
|
||||
self.raw_output.setMaximumHeight(100)
|
||||
raw_layout.addWidget(self.raw_output)
|
||||
|
||||
|
||||
self.raw_clear_btn = QPushButton("Очистить логи")
|
||||
self.raw_clear_btn.clicked.connect(self.clear_logs)
|
||||
raw_layout.addWidget(self.raw_clear_btn)
|
||||
|
||||
raw_group.setLayout(raw_layout)
|
||||
layout.addWidget(raw_group)
|
||||
|
||||
self.setLayout(layout)
|
||||
def memory_write(self):
|
||||
"""Запись в память (POKE)"""
|
||||
try:
|
||||
device_addr = 10
|
||||
mem_addr = int(self.mem_addr_edit.text(), 16)
|
||||
data = int(self.mem_data_edit.text(), 16)
|
||||
|
||||
success, result = self.raw_protocol.poke(mem_addr, data, device_addr)
|
||||
|
||||
if success:
|
||||
self.raw_output.append(f"POKE 0x{mem_addr:04X} = 0x{data:04X}")
|
||||
self.raw_output.append(f"Ответ: {result['raw'].hex()}")
|
||||
else:
|
||||
self.raw_output.append(f"Ошибка POKE: {result}")
|
||||
|
||||
except ValueError as e:
|
||||
self.raw_output.append(f"Ошибка в данных: {e}")
|
||||
|
||||
def memory_read(self):
|
||||
"""Чтение из памяти (PEEK)"""
|
||||
try:
|
||||
device_addr = 10
|
||||
mem_addr = int(self.mem_addr_edit.text(), 16)
|
||||
|
||||
success, result = self.raw_protocol.peek(mem_addr, device_addr)
|
||||
|
||||
if success:
|
||||
value = result['value']
|
||||
self.mem_result_label.setText(f"0x{value:04X} ({value})")
|
||||
self.raw_output.append(f"PEEK 0x{mem_addr:04X} = 0x{value:04X}")
|
||||
self.raw_output.append(f"Ответ: {result['raw_response']['raw'].hex()}")
|
||||
else:
|
||||
self.mem_result_label.setText(f"Ошибка: {result}")
|
||||
self.raw_output.append(f"Ошибка PEEK: {result}")
|
||||
|
||||
except ValueError as e:
|
||||
self.mem_result_label.setText(f"Ошибка: {e}")
|
||||
self.raw_output.append(f"Ошибка в данных: {e}")
|
||||
|
||||
def send_raw_data(self):
|
||||
"""Отправка сырых данных"""
|
||||
hex_str = self.raw_send_edit.text()
|
||||
success, result = self.raw_protocol.send_raw(hex_str)
|
||||
|
||||
if success:
|
||||
self.raw_output.append(result)
|
||||
else:
|
||||
self.raw_output.append(f"Ошибка: {result}")
|
||||
|
||||
def read_raw_data(self):
|
||||
"""Чтение сырых данных"""
|
||||
success, result = self.raw_protocol.receive_raw()
|
||||
|
||||
if success:
|
||||
self.raw_output.append(f"Получено: {result}")
|
||||
else:
|
||||
self.raw_output.append(f"Ошибка: {result}")
|
||||
|
||||
def append_text(self, text):
|
||||
"""Добавить текст в вывод (используется SerialTab)"""
|
||||
if text:
|
||||
self.raw_output.append(text)
|
||||
|
||||
|
||||
def clear_logs(self):
|
||||
"""Очистить лог"""
|
||||
self.raw_output.clear()
|
||||
659
serialbus.py
Normal file
659
serialbus.py
Normal 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
|
||||
Reference in New Issue
Block a user