Files
Andrey bf5f094249 Расширить DEIF и генератор сигналов, подключить исходники KDiff3
Добавить общую модель сигналов XY и адаптировать генератор к двум каналам.
Расширить Android-порт DEIF общим контроллером рабочей области и проверками.
Подключить официальный репозиторий KDiff3 вложенным подмодулем.
2026-10-29 23:32:03 +03:00

132 lines
12 KiB
Python

# Рабочий транспорт генератора по USB CDC/COM для WG и блочной загрузки SET v2. Ожидания
# выполняются в рабочем потоке; результаты возвращаются через сигналы, сохраняя отзывчивость
# интерфейса.
"""USB CDC/COM worker for WG v1/v2/v3, with SET v2 block uploads. All waits run in a worker, never the GUI thread."""
import time
from PySide6.QtCore import QObject, QRunnable, Signal, QIODevice
from PySide6.QtSerialPort import QSerialPort
from set_devices.wavegen_protocol import WaveProtocol
class _Signals(QObject):
finished = Signal(object, str)
progress = Signal(int)
class WaveTransfer(QRunnable):
def __init__(self, port_name, address, action, waveform=None, port_factory=None):
super().__init__()
self.port_name, self.address, self.action, self.waveform = port_name, address, action, waveform
self.signals = _Signals()
self.port_factory = port_factory or QSerialPort
def run(self):
port = self.port_factory()
try:
protocol = WaveProtocol(self.address)
port.setPortName(self.port_name)
port.setBaudRate(115200)
port.setDataBits(QSerialPort.Data8)
port.setParity(QSerialPort.NoParity)
port.setStopBits(QSerialPort.OneStop)
port.setFlowControl(QSerialPort.NoFlowControl)
if not port.open(QIODevice.ReadWrite):
raise OSError(port.errorString())
port.clear()
def transfer(request, decode):
if port.write(request) != len(request):
raise OSError(port.errorString())
if port.bytesToWrite() and not port.waitForBytesWritten(1000):
raise OSError("Таймаут передачи USB")
reply = bytearray()
budget = getattr(port, 'response_timeout', lambda ms: ms)(1500)
deadline = time.monotonic() + budget / 1000
while time.monotonic() < deadline:
if port.bytesAvailable() or port.waitForReadyRead(50):
reply.extend(bytes(port.readAll()))
response = decode(request, reply)
if response is not None:
return response
raise OSError("Нет подтверждения USB. Состояние выхода неизвестно; проверьте связь и нажмите Стоп.")
def exchange(operation, index=0, value=0):
return transfer(protocol.request(operation, index, value), protocol.response)
def read_status():
words = exchange(0)
result = protocol.status(words)
if result['version'] >= 2:
result = protocol.status(words, exchange(8)[0])
try: # Дополнительные регистры объявляют XY независимо от версии прежнего восьмисловного статуса.
channels,selected,maximum,other=exchange(12) # Четыре слова 0x1309…0x130C читаются общим C-кодеком.
result.update(channels=channels,selected=selected,max_channels=maximum,received_other=other) # Сохраняет фактические возможности порта и состояние обеих таблиц.
except ValueError as error: # Старые WG v1/v2/v3 отвечают исключением адреса либо имеют прежний native-кодек.
if "(-102)" not in str(error) and "Недопустимая команда" not in str(error): # Ошибки CRC и структуры ответа нельзя скрывать как одноканальную плату.
raise # Сохраняет штатный отказ передачи при повреждённом ответе.
result.update(channels=1,max_channels=1) # Старый клиент и прошивка сохраняют одноканальную работу без новых записей.
return result # Возможности XY проверяются до stop и изменения таблиц устройства.
status = read_status() # Positive device identification before any write.
if self.action == "upload":
wave = self.waveform
if wave is None or len(wave.codes) > status["capacity"]:
raise ValueError("Таблица не помещается в память устройства")
if status['version'] == 1 and wave.sample_rate > 50000:
raise ValueError("Для частоты выше 50 000 отсчётов/с обновите прошивку МК: нужна карта генератора версии 2")
channel_codes=(wave.codes,) if not hasattr(wave,"codes_y") else (wave.codes,wave.codes_y) # Одна временная сетка содержит один канал либо обе координаты XY.
if len(channel_codes)>status["max_channels"]: # Устройство обязано явно подтвердить аппаратный порт второго канала.
raise ValueError("Для XY нужны прошивка F407/HOPE и native-кодек templates с поддержкой двух каналов") # Отказ происходит до остановки ранее работающего выхода.
if any(len(codes)!=len(wave.codes) for codes in channel_codes): # Нельзя загружать пару с разными длинами даже из вручную созданного задания.
raise ValueError("X и Y должны иметь одинаковую длину") # Не меняет оборудование при несовместимой временной сетке.
exchange(1)
if status["max_channels"]>=2: # Новый порт явно сбрасывает формат перед каждой загрузкой, включая возврат XY→mono.
exchange(10,value=len(channel_codes)) # Выбирает число одновременно воспроизводимых каналов и инвалидирует обе прежние таблицы.
if status['version'] >= 2:
exchange(9, value=wave.sample_rate >> 16)
exchange(2, value=wave.sample_rate & 0xffff)
exchange(3, value=len(wave.codes))
blocks = status['version'] >= 3 and getattr(port, 'supports_wave_blocks', False)
step = 120 if blocks else 1
total=len(wave.codes)*len(channel_codes)*2 # Прогресс учитывает запись и чтение каждого кода обеих координат.
completed=0 # Общий счётчик исключает откат процентов при переходе от X к Y.
for channel,codes in enumerate(channel_codes): # Загружает и проверяет X, затем Y без промежуточного пуска.
if status["max_channels"]>=2: # Прежняя прошивка не получает неподдерживаемый селектор окна.
exchange(11,value=channel) # Одинаковые block/sample команды адресуют выбранную координату через общий регистр 0x130A.
for reading in (False,True): # Обратное чтение всей координаты обязательно до общего commit пары.
for index in range(0,len(codes),step): # Блочный транспорт ограничен прежними 120 кодами на запрос.
values=codes[index:index+step] # Последний блок содержит только действительные отсчёты без дополнения.
if reading: # Проверка использует ту же границу блока и тот же селектор, что запись.
actual=transfer(protocol.block_request(index,count=len(values)),protocol.block_response) if blocks else exchange(7,index) # Читает реальные коды из памяти устройства.
if tuple(actual)!=tuple(values): # Любое расхождение запрещает общий commit и дальнейший пуск.
raise ValueError(f"Проверка {'XY'[channel]} не пройдена: отсчёты с {index}") # Показывает ошибочную координату и начало блока.
elif blocks: # SET v2 переносит тот же атомарно проверяемый RTU-блок из общего ядра templates.
transfer(protocol.block_request(index,values),protocol.block_response) # Подтверждает запись всего блока до следующего запроса.
else: # Старый UART/RTU клиент сохраняет поотсчётную загрузку без смены wire-контракта.
exchange(4,index,values[0]) # Записывает очередной код выбранной координаты.
completed+=len(values) # Каждый подтверждённый код учитывается ровно один раз на фазу.
self.signals.progress.emit(int(completed*100/total)) # Общий прогресс монотонно достигает 100 % после проверки обеих таблиц.
if status["max_channels"]>=2: # Итоговый статус возвращается к X для одинакового отображения mono и XY.
exchange(11,value=0) # Не меняет содержимое пар и не запускает аппаратный вывод.
exchange(5)
elif self.action == "start":
if not status["ready"]:
raise ValueError("Сначала загрузите и проверьте таблицу")
exchange(6)
elif self.action == "stop":
exchange(1)
elif self.action != "status":
raise ValueError("Неизвестное действие")
result = read_status()
if self.action == "upload" and (not result["ready"] or result["running"] or
result.get("channels",1)!=(2 if hasattr(self.waveform,"codes_y") else 1) or # Итоговое число каналов обязано совпасть с подготовленным mono/XY заданием.
result["count"] != len(self.waveform.codes) or result["rate"] != self.waveform.sample_rate):
raise ValueError("Устройство не подтвердило загруженную таблицу")
if self.action in ("start", "stop") and result["running"] != (self.action == "start"):
raise ValueError("Устройство не подтвердило состояние выхода")
self.signals.progress.emit(100)
self.signals.finished.emit(result, "")
except Exception as error:
self.signals.finished.emit(None, str(error))
finally:
port.close()