Extract reusable device protocols, ports and logic analyzers from SETGUI

This commit is contained in:
2026-09-23 20:11:37 +03:00
parent 80ba17d77d
commit 795a1279b1
63 changed files with 10181 additions and 6 deletions

View File

@@ -0,0 +1,187 @@
"""@file mock_port.py
@brief Qt-таймер, имитирующий подключённый контроллер без COM-порта.
Реализует mock-устройство для тестирования и разработки без физического
оборудования. Использует детерминированные значения для надёжного тестирования.
"""
from __future__ import annotations
from .qt_compat import QElapsedTimer, QObject, QTimer, Signal
from set_devices.ds18b20 import MockSensorBus
from set_devices.eeprom import CatalogEntry, MemoryInfo
from set_devices.models import MockSignalSource, SignalCatalog
from set_devices.panel import MockPanel
class MockDevicePort(QObject):
"""@brief Асинхронный mock-порт с интерфейсом реального порта.
Qt-таймер является платформенным планировщиком, а генерация значений
делегируется переносимому ``MockSignalSource``.
"""
connected_changed = Signal(bool, str)
snapshot_received = Signal(object)
sensors_received = Signal(object)
memory_info_received = Signal(object)
catalog_received = Signal(object)
sensor_list_received = Signal(object)
panel_received = Signal(object)
tx_logged = Signal(str)
rx_logged = Signal(str)
error_occurred = Signal(str)
def __init__(self, catalog: SignalCatalog, parent: QObject | None = None) -> None:
super().__init__(parent)
self._source = MockSignalSource(catalog)
self._bus = MockSensorBus()
# Каталог mock-прибора: два датчика уже разложены по позициям одной
# сборки (assembly_serial=7), третий остаётся без записи.
self._catalog = [
CatalogEntry(rom=self._bus.roms()[0], position=1, assembly_serial=7),
CatalogEntry(rom=self._bus.roms()[2], position=258, assembly_serial=7),
]
self._panel = MockPanel(self._panel_rows)
self._elapsed = QElapsedTimer()
self._timer = QTimer(self)
self._timer.setInterval(250)
self._timer.timeout.connect(self._publish_snapshot)
def open(self) -> None:
"""@brief Запускает генерацию четырёх снимков в секунду.
Повторный вызов безопасен и не создаёт второй таймер.
"""
if self._timer.isActive():
return
self._elapsed.start()
self._timer.start()
self.connected_changed.emit(True, "MOCK")
self.rx_logged.emit("Mock-контроллер подключён")
self._publish_snapshot()
def close(self) -> None:
"""@brief Останавливает генератор и публикует отключение.
Повторное закрытие не создаёт ложное событие изменения состояния.
"""
was_active = self._timer.isActive()
self._timer.stop()
if was_active:
self.connected_changed.emit(False, "MOCK")
def scan_sensors(self) -> None:
"""@brief Публикует список ROM mock-шины 1-Wire.
Поиск выполняется мгновенно: модель шины известна заранее.
"""
self.tx_logged.emit("SENSOR_SCAN mock")
self.sensor_list_received.emit(self._bus.roms())
self.publish_sensors()
def publish_sensors(self) -> None:
"""@brief Публикует очередной набор измерений DS18B20."""
self.sensors_received.emit(self._bus.snapshot(self._timestamp_s()))
def scan_memory(self) -> None:
"""@brief Публикует состояние mock-памяти и каталога позиций."""
self.tx_logged.emit("EEPROM_SCAN mock")
self.memory_info_received.emit(
MemoryInfo(
present=True,
persistent=True,
count=len(self._catalog),
capacity=32,
i2c_address=0x50,
page_size=64,
size=32768,
base_address=1024,
bus_errors=0,
blob_size=332,
)
)
def read_catalog(self) -> None:
"""@brief Публикует записи каталога mock-прибора."""
self.tx_logged.emit("EEPROM_READ mock")
self.catalog_received.emit(list(self._catalog))
def set_user_bytes(self, rom: bytes, user_byte1: int, user_byte2: int) -> None:
"""@brief Применяет пользовательские байты к mock-датчику.
@param rom Идентификатор датчика на mock-шине.
@param user_byte1 Новое значение TH.
@param user_byte2 Новое значение TL.
"""
try:
self._bus.set_user_bytes(rom, user_byte1, user_byte2)
except (KeyError, ValueError) as error:
self.error_occurred.emit(str(error))
return
self.tx_logged.emit(f"SET_USER_BYTES mock {user_byte1:#04x} {user_byte2:#04x}")
self.publish_sensors()
def set_resolution(self, rom: bytes, bits: int) -> None:
"""@brief Меняет разрешение mock-датчика.
@param rom Идентификатор датчика на mock-шине.
@param bits Разрешение 9..12 бит.
"""
try:
self._bus.set_resolution(rom, bits)
except (KeyError, ValueError) as error:
self.error_occurred.emit(str(error))
return
self.tx_logged.emit(f"SET_RESOLUTION mock {bits} бит")
self.publish_sensors()
def read_panel(self) -> None:
"""@brief Публикует снимок экрана mock-панели."""
self.panel_received.emit(self._panel.screen())
def press_panel_key(self, key: int, hold: bool = False) -> None:
"""@brief Применяет нажатие кнопки к mock-панели.
@param key Код кнопки из PanelKey.
@param hold Длинное нажатие.
"""
self._panel.press(key, hold)
self.tx_logged.emit(f"UI_KEY mock {int(key)}{' hold' if hold else ''}")
self.read_panel()
def _panel_rows(self) -> list[tuple[str, str]]:
"""Строки экрана датчиков mock-панели: номер и температура."""
rows: list[tuple[str, str]] = []
for index, reading in enumerate(self._bus.snapshot(self._timestamp_s())):
temperature = reading.temperature
rows.append((
f"{index + 1} {reading.serial}",
"---" if temperature is None else f"{temperature:+.1f}",
))
return rows
def set_output(self, key: str, value: bool) -> None:
"""@brief Применяет управляющую дискрету к mock-модели.
@param key Ключ разрешённого выхода.
@param value Новое состояние; ошибка неизвестного ключа идёт в signal.
"""
try:
self._source.set_output(key, value)
except KeyError as error:
self.error_occurred.emit(str(error))
return
self.tx_logged.emit(f"{key} = {int(value)}")
self._publish_snapshot()
def _timestamp_s(self) -> float:
"""Возвращает монотонное время mock-порта в секундах."""
return self._elapsed.elapsed() / 1000.0
def _publish_snapshot(self) -> None:
"""@brief Создаёт и публикует атомарный снимок вне UI-виджетов."""
timestamp_s = self._timestamp_s()
self.snapshot_received.emit(self._source.snapshot(timestamp_s))
self.sensors_received.emit(self._bus.snapshot(timestamp_s))