94 lines
14 KiB
Python
94 lines
14 KiB
Python
"""Единый неблокирующий порт CAN485 для USB CDC/UART и Ethernet TCP.""" # Протокол кадров остаётся в can485_board и переносимом C-ядре templates.
|
|
from .qt_compat import QObject, Signal, QTimer, QSerialPort, QElapsedTimer # Qt elapsed timer ограничивает также короткое подтверждение восстановления UART-консоли.
|
|
try: # Выбирает тот же Qt binding, который использует остальное приложение.
|
|
from PySide6.QtNetwork import QTcpSocket, QAbstractSocket # TCP передаёт тот же поток строк, что физический USB-порт.
|
|
except ImportError: # Старые системы используют доступный PySide2.
|
|
from PySide2.QtNetwork import QTcpSocket, QAbstractSocket # Публичный контракт адаптера не зависит от версии Qt.
|
|
|
|
|
|
class Can485Port(QObject): # Совместимая с BoardTab оболочка исключает копирование протокольных и экранных обработчиков.
|
|
readyRead = Signal() # Уведомляет монитор о произвольном фрагменте, включая неполную строку.
|
|
errorOccurred = Signal(object) # Использует существующий обработчик ошибок последовательного порта панели.
|
|
connected = Signal() # TCP-подключение подтверждается асинхронно после фактического установления соединения.
|
|
|
|
def __init__(self, parent=None): # Создаёт закрытые endpoints без аппаратного ввода-вывода при импорте или открытии вкладки.
|
|
super().__init__(parent) # QObject связывает жизненный цикл сокетов с владельцем панели.
|
|
self.serial = QSerialPort(self); self.tcp = QTcpSocket(self); self.kind = 'USB'; self._closing = False; self._error = ''; self._endpoint = ''; self._console = False; self._console_ready = False; self._console_rx = bytearray(); self.console_restored=True # Состояние восстановления консоли проверяется аппаратным USB-тестом после закрытия.
|
|
self.serial.readyRead.connect(self._serial_ready); self.tcp.readyRead.connect(self.readyRead) # USB-консоль ожидает подтверждение службы, затем передаёт bytes штатному parser строк.
|
|
self.serial.errorOccurred.connect(self._serial_error); self.tcp.errorOccurred.connect(self._tcp_error); self.tcp.disconnected.connect(self._disconnected) # Потеря соединения останавливает циклическую отправку через обработчик панели.
|
|
self.tcp.connected.connect(self._connected) # Начальная настройка выполняется только после подтверждённого TCP handshake.
|
|
self.timeout = QTimer(self); self.timeout.setSingleShot(True); self.timeout.timeout.connect(self._timed_out) # Конечный срок TCP не блокирует GUI даже при отключённой плате.
|
|
|
|
def _connected(self): # Завершает фазу ожидания и разрешает запрос версии.
|
|
self.timeout.stop(); self.connected.emit() # Запоздалый timeout больше не может закрыть действующую сессию.
|
|
|
|
def _tcp_error(self, error): # Преобразует ошибку TCP в общий контракт порта.
|
|
if not self._closing: self._error = self.tcp.errorString(); self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # GUI получает исходную диагностическую строку сокета.
|
|
|
|
def _serial_error(self, error): # Сохраняет штатные коды QSerialPort, включая отсутствие ошибки.
|
|
if not self._closing: self._error = self.serial.errorString(); self.errorOccurred.emit(error) # USB-обрыв проходит тот же cleanup, что сетевой.
|
|
|
|
def _disconnected(self): # FIN от платы завершает сессию даже без отдельного socket error.
|
|
if not self._closing: self._error = 'Ethernet: соединение закрыто платой'; self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Панель прекращает периодическую передачу и очищает подтверждённые настройки.
|
|
|
|
def _timed_out(self): # Срабатывает только для незавершённого TCP-подключения.
|
|
if not self.isOpen(): self._error = 'Нет ответа CAN-службы Zynq за 5 секунд. Для USB-консоли нужны root shell и установленная zynq-can485d.'; self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Ожидание отсутствующего сервиса либо login prompt не блокирует GUI.
|
|
|
|
def _serial_ready(self): # В режиме консоли отделяет готовность туннеля от простого открытия COM-порта.
|
|
if self._closing: return # Во время короткого ожидания shell prompt данные читает cleanup, не вызывая повторные GUI callbacks.
|
|
if not self._console: self.readyRead.emit(); return # Прямой USB CDC/UART сохраняет исходные bytes без буферной копии.
|
|
self._console_rx.extend(bytes(self.serial.readAll())) # Shell и nc могут выдавать произвольные фрагменты строк.
|
|
if not self._console_ready and b'#INFO Zynq CAN485 / templates' in self._console_rx: self._console_ready=True; self.timeout.stop(); self.connected.emit() # Команды CAN разрешаются только после реального ответа общего daemon.
|
|
if self._console_ready: self.readyRead.emit() # После подтверждения весь поток, включая сохранённый заголовок, поступает штатному монитору.
|
|
elif len(self._console_rx)>8192: del self._console_rx[:-4096] # До handshake хранит ограниченный хвост консольного вывода.
|
|
|
|
def start_console_tunnel(self, port=37579): # Использует уже открытую root UART-консоль как USB-канал к тому же TCP-порту службы.
|
|
port=int(port) # Числовой аргумент проверяется до формирования команды shell.
|
|
if not 1<=port<=65535: raise ValueError('TCP-порт CAN должен быть 1..65535') # Исключает неверный endpoint и внедрение shell-команд из параметра.
|
|
self._console=True; self._console_ready=False; self._console_rx.clear(); self.console_restored=False; self.timeout.start(5000) # Открытый tty не выдаётся за подтверждённый CAN-сеанс.
|
|
self.serial.write(f'\rstty -echo -icanon min 1 time 0; nc 127.0.0.1 {port}; stty sane\r'.encode('ascii')) # Штатный nc передаёт общий протокол без копии daemon и без перенастройки USB/Device Tree.
|
|
|
|
def connect_tcp(self, host, port): # Endpoint уже проверен UI, DNS и синхронное ожидание не требуются.
|
|
self.close(); self.kind = 'Ethernet'; self._endpoint = f'{host}:{port}'; self._error = ''; self.timeout.start(5000); self.tcp.connectToHost(host, port) # Пять секунд ограничивают ожидание недоступного TCP-порта 37579.
|
|
|
|
def isOpen(self): # Штатные кнопки BoardTab учитывают реальное состояние выбранного канала.
|
|
return self.tcp.state() == QAbstractSocket.SocketState.ConnectedState if self.kind == 'Ethernet' else self.serial.isOpen() and (not self._console or self._console_ready) # Открытый USB tty с незавершённым nc handshake тоже не считается CAN-подключением.
|
|
|
|
def isConnecting(self): # Позволяет отменить незавершённую попытку подключения повторным нажатием кнопки.
|
|
return self.timeout.isActive() # Таймер активен только в фазе TCP handshake.
|
|
|
|
def close(self): # Закрытие идемпотентно и не порождает рекурсивную ошибку панели.
|
|
self._closing = True; self.timeout.stop(); self.tcp.abort() # Сначала отменяет deadline и сетевые сигналы старой сессии.
|
|
if self._console and self.serial.isOpen(): # Дожидается реального ответа UART, чтобы close не отбросил ещё не переданный Ctrl-C в драйвере Windows.
|
|
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # Удаляет старый поток статуса и немедленно передаёт сигнал завершения собственного foreground nc.
|
|
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Ограничивает ожидание освобождения локальной выходной очереди.
|
|
elapsed=QElapsedTimer(); elapsed.start(); response=bytearray() # Подтверждение восстановления shell имеет конечный срок 400 мс.
|
|
while elapsed.elapsed()<400 and not self.console_restored: # Нельзя закрыть FTDI раньше передачи управляющего байта и возврата root prompt.
|
|
self.serial.waitForReadyRead(40); response.extend(bytes(self.serial.readAll())); self.console_restored=response.rstrip(b'\r\n').endswith((b'# ',b'$ ')) # Настоящий prompt подтверждает завершение nc и выполнение stty sane.
|
|
if not self.console_restored: self._error='Закрыт USB, но возврат Linux-консоли за 400 мс не подтверждён' # Не выдаёт простое закрытие COM за подтверждённое восстановление терминала.
|
|
if self.serial.isOpen(): self.serial.close() # Не вызывает NotOpenError при повторном cleanup уже закрытого USB-порта.
|
|
self._console=False; self._console_ready=False; self._console_rx.clear(); self._closing = False # Не оставляет старые данные handshake для следующего COM-порта.
|
|
|
|
def readAll(self): # Возвращает сырой QByteArray для существующего LineAssembler templates.
|
|
if self._console: data=bytes(self._console_rx); self._console_rx.clear(); return data # Консольные bytes прочитаны единожды при поиске подтверждённого заголовка.
|
|
return self.tcp.readAll() if self.kind == 'Ethernet' else self.serial.readAll() # Сегментация TCP/USB не влияет на протокол.
|
|
|
|
def write(self, data): # Длина результата позволяет обнаружить непринятую целиком команду.
|
|
return self.tcp.write(data) if self.kind == 'Ethernet' else self.serial.write(data) # Порт не добавляет заголовков либо переводов строк к готовой команде.
|
|
|
|
def errorString(self): # Единая диагностика сохраняется до следующего соединения.
|
|
return self._error or (self.tcp.errorString() if self.kind == 'Ethernet' else self.serial.errorString()) # Сообщение Qt не подменяется общим «нет связи».
|
|
|
|
def portName(self): # Нижняя строка монитора показывает фактический канал.
|
|
return self._endpoint if self.kind == 'Ethernet' else self.serial.portName() # Сетевое соединение отличается от COM-порта платы.
|
|
|
|
def setPortName(self, name): # Штатный USB-путь BoardTab выбирает физический порт без изменения своего кода.
|
|
self.kind = 'USB'; self.serial.setPortName(name) # Следующие настройки применяются только к закрытому USB-порту.
|
|
|
|
def setBaudRate(self, value): return self.serial.setBaudRate(value) # Физический USB-UART требует согласованной скорости, USB CDC принимает line coding.
|
|
def setDataBits(self, value): return self.serial.setDataBits(value) # Использует 8 бит данных штатной панели.
|
|
def setParity(self, value): return self.serial.setParity(value) # Сохраняет отсутствие parity в общем контракте 8N1.
|
|
def setStopBits(self, value): return self.serial.setStopBits(value) # Сохраняет один стоповый бит.
|
|
def setFlowControl(self, value): return self.serial.setFlowControl(value) # RTS/CTS не задействуются для управления платой.
|
|
def open(self, mode): return self.serial.open(mode) # Открытие USB выполняется штатным QSerialPort с исходной диагностикой ошибок.
|