Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
346 lines
19 KiB
Python
346 lines
19 KiB
Python
"""Qt transport over the common native TMS2812 codec."""
|
|
from PySide6.QtWidgets import *
|
|
from PySide6.QtCore import *
|
|
from PySide6.QtGui import *
|
|
from PySide6.QtSerialPort import QSerialPort
|
|
from protocan import tms_terminal as memory # Существующий кодек сохраняет формат запросов памяти и проверку CRC.
|
|
from .exchange_queue import exchange_queue # RAW и обращения памяти разделяют очередь с шинами и макросами.
|
|
|
|
|
|
class RawProtocol:
|
|
def __init__(self, host=None):
|
|
self.host = host
|
|
self.serial_port = QSerialPort() if host is None else None
|
|
self.buffer = bytearray()
|
|
self._loop = None
|
|
self._callback = None
|
|
self._command = None
|
|
self._error = None
|
|
self._packet = None
|
|
self._raw_rx = bytearray()
|
|
if host is None:
|
|
self.serial_port.readyRead.connect(self._receive)
|
|
|
|
@property
|
|
def is_connected(self):
|
|
return self.port.isOpen()
|
|
|
|
@property
|
|
def port(self):
|
|
return self.host._serial if self.host else self.serial_port
|
|
|
|
def connect(self, port_name, baudrate=115200):
|
|
self.port.setPortName(port_name)
|
|
self.port.setBaudRate(baudrate)
|
|
return self.port.open(QIODevice.ReadWrite)
|
|
|
|
def disconnect(self): # Отключение снимает все задания данного соединения.
|
|
exchange_queue(self).clear() # Неотправленные операции не должны перейти на новое соединение.
|
|
self.cancel_async("COM отключён") # Ожидающий callback завершается реальной ошибкой закрытия порта.
|
|
self.port.close() # После завершения состояния обмена закрываем существующий UART.
|
|
|
|
def _receive(self):
|
|
self.feed(bytes(self.port.readAll()))
|
|
|
|
def feed(self, raw):
|
|
self._raw_rx.extend(raw)
|
|
del self._raw_rx[:-65536]
|
|
if self._loop is None and self._callback is None:
|
|
return
|
|
self.buffer.extend(raw)
|
|
try:
|
|
packet = memory.memory_response(self.buffer, self._controller, self._command)
|
|
if packet is not None:
|
|
self._packet = packet
|
|
if len(packet) == (10 if self._command == memory.PEEK else 6):
|
|
self._settle.stop()
|
|
self._finish_async() if self._callback else self._loop.quit()
|
|
elif not self._settle.isActive():
|
|
self._settle.start(30)
|
|
except ValueError as error:
|
|
self._error = str(error)
|
|
self._finish_async() if self._callback else self._loop.quit()
|
|
|
|
def request_async(self, address, value, device_address, callback):
|
|
"""Begin one request and return immediately; completion runs on the Qt thread."""
|
|
if not self.is_connected:
|
|
callback(False, "Подключите COM-порт")
|
|
return
|
|
if self._loop is not None or self._callback is not None or (self.host and self.host._pending_address is not None):
|
|
callback(False, "Дождитесь завершения текущего обмена")
|
|
return
|
|
self._controller = (device_address if device_address is not None else
|
|
self.host.address.value() if self.host else 1)
|
|
self._command = memory.PEEK if value is None else memory.POKE
|
|
try:
|
|
request = memory.request(self._controller, address, value)
|
|
except (ValueError, RuntimeError) as error:
|
|
callback(False, str(error))
|
|
return
|
|
self.buffer.clear()
|
|
self._error = self._packet = None
|
|
self._callback = callback
|
|
self._settle = QTimer()
|
|
self._settle.setSingleShot(True)
|
|
self._settle.timeout.connect(self._finish_async)
|
|
self._async_timer = QTimer()
|
|
self._async_timer.setSingleShot(True)
|
|
self._async_timer.timeout.connect(lambda: self.cancel_async("Нет ответа от устройства"))
|
|
if self.host:
|
|
self.host._pending_address = self._controller
|
|
self.host._pending_kind = "pysetterminal"
|
|
self.host._log("TX", request)
|
|
self.port.readAll()
|
|
self._async_timer.start(self.host.timeout_ms.value() if self.host else 1000)
|
|
if self.port.write(request) != len(request):
|
|
self.cancel_async("Ошибка передачи")
|
|
|
|
def _finish_async(self):
|
|
callback = self._callback
|
|
if callback is None:
|
|
return
|
|
self._callback = None
|
|
self._async_timer.stop()
|
|
self._settle.stop()
|
|
if self.host:
|
|
self.host._pending_address = self.host._pending_kind = None
|
|
if self._error or self._packet is None:
|
|
callback(False, self._error or "Обмен прерван")
|
|
return
|
|
packet = self._packet
|
|
result = {'address': self._controller, 'command': self._command, 'raw': packet}
|
|
if self._command == memory.PEEK:
|
|
result = {'value': memory.decode_reply(packet, self._controller, self._command), 'raw_response': result}
|
|
callback(True, result)
|
|
|
|
def cancel_async(self, reason="Макрос остановлен"):
|
|
if self._callback is not None:
|
|
self._error = reason
|
|
self._finish_async()
|
|
|
|
def _exchange(self, address, value, device_address):
|
|
if not self.is_connected:
|
|
return False, "Подключите COM-порт"
|
|
if self._loop is not None or self._callback is not None or (self.host and self.host._pending_address is not None):
|
|
return False, "Дождитесь завершения текущего обмена"
|
|
self._controller = (device_address if device_address is not None else
|
|
self.host.address.value() if self.host else 1)
|
|
self._command = memory.PEEK if value is None else memory.POKE
|
|
try:
|
|
request = memory.request(self._controller, address, value)
|
|
except (ValueError, RuntimeError) as error:
|
|
return False, str(error)
|
|
self.buffer.clear()
|
|
self._error = self._packet = None
|
|
loop = QEventLoop()
|
|
timer = QTimer()
|
|
timer.setSingleShot(True)
|
|
timer.timeout.connect(loop.quit)
|
|
self._loop = loop
|
|
self._settle = QTimer()
|
|
self._settle.setSingleShot(True)
|
|
self._settle.timeout.connect(loop.quit)
|
|
if self.host:
|
|
self.host._pending_address = self._controller
|
|
self.host._pending_kind = "pysetterminal"
|
|
self.host._log("TX", request)
|
|
try:
|
|
self.port.readAll()
|
|
if self.port.write(request) != len(request):
|
|
return False, "Ошибка передачи"
|
|
if self._error is None and (self._packet is None or self._settle.isActive()):
|
|
timer.start(self.host.timeout_ms.value() if self.host else 1000)
|
|
loop.exec()
|
|
if self._error or self._packet is None:
|
|
return False, self._error or "Нет ответа от устройства"
|
|
packet = self._packet
|
|
result = {'address': self._controller, 'command': self._command, 'raw': packet}
|
|
if value is None:
|
|
return True, {'value': memory.decode_reply(packet, self._controller, self._command), 'raw_response': result}
|
|
return True, result
|
|
finally:
|
|
timer.stop()
|
|
self._settle.stop()
|
|
self._loop = None
|
|
if self.host:
|
|
self.host._pending_address = self.host._pending_kind = None
|
|
|
|
def poke(self, mem_address, data_value, device_address=None):
|
|
return self._exchange(mem_address, data_value, device_address)
|
|
|
|
def peek(self, mem_address, device_address=None):
|
|
return self._exchange(mem_address, None, device_address)
|
|
|
|
def send_raw(self, hex_data):
|
|
if not self.is_connected:
|
|
return False, "Подключите COM-порт"
|
|
if self._loop is not None or self._callback is not None or (self.host and self.host._pending_address is not None):
|
|
return False, "Дождитесь завершения текущего обмена"
|
|
if self.host and self.host._cycle_timer.isActive():
|
|
return False, "Остановите циклический обмен перед RAW"
|
|
try:
|
|
data = bytes.fromhex(hex_data)
|
|
except ValueError:
|
|
return False, "Некорректные HEX данные"
|
|
if not data:
|
|
return False, "Введите HEX данные"
|
|
if self.port.write(data) != len(data):
|
|
return False, "Ошибка передачи"
|
|
if self.host:
|
|
self.host._log("TX", data)
|
|
return True, "TX " + data.hex(" ")
|
|
|
|
def receive_raw(self, max_bytes=1024):
|
|
data = bytes(self._raw_rx[:max_bytes])
|
|
del self._raw_rx[:max_bytes]
|
|
return (True, data.hex(" ")) if data else (False, "Нет данных")
|
|
|
|
|
|
class RawProtocolWidget(QWidget):
|
|
"""Виджет для отладки RAW протокола (всегда виден внизу окна)"""
|
|
|
|
def __init__(self, raw_protocol, parent=None, *, device_address=10):
|
|
super().__init__(parent)
|
|
self.raw_protocol = raw_protocol
|
|
self.device_address = device_address
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setProperty("fixedPanelLayout", True)
|
|
layout.setContentsMargins(4, 4, 4, 4)
|
|
layout.setSpacing(4)
|
|
|
|
# Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE)
|
|
mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)")
|
|
mem_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
|
mem_layout = QGridLayout()
|
|
mem_layout.setContentsMargins(6, 6, 6, 6)
|
|
mem_layout.setSpacing(4)
|
|
|
|
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_layout.setColumnStretch(4, 1)
|
|
mem_group.setLayout(mem_layout)
|
|
layout.addWidget(mem_group)
|
|
|
|
# Группа для RAW отправки
|
|
raw_group = QGroupBox("RAW отправка/чтение")
|
|
raw_layout = QVBoxLayout()
|
|
raw_layout.setProperty("fixedPanelLayout", True)
|
|
raw_layout.setContentsMargins(6, 6, 6, 6)
|
|
raw_layout.setSpacing(4)
|
|
|
|
self.raw_send_edit = QLineEdit()
|
|
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
|
|
self.raw_send_edit.setMaximumWidth(640)
|
|
raw_layout.addWidget(self.raw_send_edit)
|
|
|
|
hbox = QHBoxLayout()
|
|
hbox.setSpacing(4)
|
|
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)
|
|
|
|
|
|
hbox.addStretch()
|
|
raw_layout.addLayout(hbox)
|
|
|
|
self.raw_output = QTextEdit()
|
|
self.raw_output.setReadOnly(True)
|
|
self.raw_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
raw_layout.addWidget(self.raw_output, 1)
|
|
|
|
|
|
self.raw_clear_btn = QPushButton("Очистить логи")
|
|
self.raw_clear_btn.clicked.connect(self.clear_logs)
|
|
raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft)
|
|
for button in (self.mem_write_btn, self.mem_read_btn, self.raw_send_btn,
|
|
self.raw_read_btn, self.raw_clear_btn):
|
|
button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
|
|
|
raw_group.setLayout(raw_layout)
|
|
layout.addWidget(raw_group, 1)
|
|
|
|
self.setLayout(layout)
|
|
def memory_write(self): # Фиксируем адрес и слово при нажатии без блокировки других элементов.
|
|
self._memory_action(True) # Запись занимает одну логическую операцию общей очереди.
|
|
|
|
def memory_read(self): # Ручное чтение памяти принимается даже во время циклического опроса шин.
|
|
self._memory_action(False) # Параметры текущего поля снимаются до ожидания соединения.
|
|
|
|
def _memory_action(self, write): # Оба направления памяти используют прежний протокол через общий планировщик.
|
|
try: # Проверка ввода выполняется до передачи любого пакета контроллеру.
|
|
controller = self.device_address() if callable(self.device_address) else self.device_address # Контроллер сохраняется на момент нажатия.
|
|
address = int(self.mem_addr_edit.text(), 16) # Адрес фиксируется независимо от последующего редактирования поля.
|
|
value = int(self.mem_data_edit.text(), 16) if write else None # Значение участвует только в WriteMemory.
|
|
if not 0 <= address <= 0xFFFFFFFF or (value is not None and not 0 <= value <= 0xFFFF): # Сохраняем разрядность адреса и шестнадцатибитного слова.
|
|
raise ValueError("Проверьте адрес и данные памяти") # Неверные поля не создают задания UART.
|
|
except ValueError as error: # Промежуточный неправильный ввод остаётся локальной ошибкой интерфейса.
|
|
self.mem_result_label.setText(str(error)) # Пользователь видит причину прямо возле результата памяти.
|
|
return # Очередь текущего соединения не изменяется.
|
|
def steps(): # Одна операция возвращает итог существующего PEEK или POKE.
|
|
result = yield address, value # Направление обмена определяется прежним соглашением None для чтения.
|
|
return result # Результат отправляется только виджету инициатора действия.
|
|
exchange_queue(self.raw_protocol).submit(steps, controller, lambda ok, reply: self._memory_done(address, value, ok, reply)) # Запрос ожидает завершения текущей логической операции шин или макроса.
|
|
|
|
def _memory_done(self, address, value, success, result): # Результат отображается по сохранённым параметрам конкретного нажатия.
|
|
if not success: # Ошибка CRC, таймаут и отключение не подменяются успешным результатом.
|
|
self.mem_result_label.setText(f"Ошибка: {result}") # Показываем отказ непосредственно в области памяти.
|
|
self.raw_output.append(f"Ошибка памяти: {result}") # История сохраняет причину неудачной операции.
|
|
elif value is None: # ReadMemory должен вернуть проверенное шестнадцатибитное слово.
|
|
word = result["value"] # Слово декодировано существующим кодеком протокола.
|
|
self.mem_result_label.setText(f"0x{word:04X} ({word})") # Показываем HEX и DEC одного результата чтения.
|
|
self.raw_output.append(f"PEEK 0x{address:04X} = 0x{word:04X}") # Адрес отражает выполненное действие, даже если поле уже изменилось.
|
|
else: # Подтверждённая запись сохраняет прежний формат диагностического журнала.
|
|
self.raw_output.append(f"POKE 0x{address:04X} = 0x{value:04X}") # Отображаем записанное слово и адрес снимка параметров.
|
|
|
|
def send_raw_data(self): # Произвольная ручная отправка также ждёт завершения целой операции шины.
|
|
text = self.raw_send_edit.text() # HEX-пакет фиксируется при нажатии кнопки отправки.
|
|
def send(): # Отправка выполняется после получения общей очередью свободного UART.
|
|
success, result = self.raw_protocol.send_raw(text) # Существующий RAW-протокол сохраняет формат и обработку ошибки порта.
|
|
self.raw_output.append(result if success else f"Ошибка: {result}") # Итог принадлежит сохранённому пакету данного нажатия.
|
|
exchange_queue(self.raw_protocol).submit_external(send, lambda: False, lambda ok, reply: None) # RAW не ожидает типизированного ответа, сохраняя прежнее поведение панели.
|
|
|
|
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()
|