Files
templates/python/set_devices/qt_ports/mock_port.py

188 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""@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))