Files
templates/python/set_devices/models.py

163 lines
6.5 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.
"""Модель нейтральных сигналов для шаблона без привязки к ТЗ."""
from __future__ import annotations
from dataclasses import dataclass, field
from math import sin
@dataclass(frozen=True)
class BitDefinition:
"""@brief Описание битового индикатора или управляющей дискреты.
@param key Стабильный ключ обмена, не зависящий от подписи в UI.
@param title Отображаемое имя сигнала.
@param writable Разрешено ли пользователю изменять значение.
@param alarm Активное состояние отображается аварийным цветом.
"""
key: str
title: str
writable: bool = False
alarm: bool = False
@dataclass(frozen=True)
class NumericDefinition:
"""@brief Метаданные цифрового поля или измерительного индикатора.
Границы и точность принадлежат catalog, поэтому UI не содержит сведений
о конкретной карте объектов контроллера.
"""
key: str
title: str
unit: str = ""
minimum: float = 0.0
maximum: float = 100.0
decimals: int = 1
writable: bool = False
@dataclass(frozen=True)
class SignalCatalog:
"""@brief Полный неизменяемый набор отображаемых сигналов.
Catalog является переносимой границей между данными устройства и Qt UI.
Для реального изделия заменяются определения, а не код вкладок.
"""
controller_fields: tuple[NumericDefinition, ...]
indicators: tuple[NumericDefinition, ...]
status_bits: tuple[BitDefinition, ...]
input_discretes: tuple[BitDefinition, ...]
output_discretes: tuple[BitDefinition, ...]
@classmethod
def template(cls) -> "SignalCatalog":
"""@brief Создаёт согласованный catalog с нейтральными именами.
@return Шаблон для обсуждения компоновки без зависимости от финального ТЗ.
"""
return cls(
controller_fields=tuple(
NumericDefinition(
key=f"controller_{index:02d}",
title=f"Параметр {index:02d}",
minimum=-9999,
maximum=9999,
decimals=0,
writable=True,
)
for index in range(1, 9)
),
indicators=tuple(
NumericDefinition(
key=f"indicator_{index:02d}",
title=f"Индикатор {index:02d}",
unit=("В" if index <= 4 else "%"),
maximum=(400.0 if index <= 4 else 100.0),
)
for index in range(1, 9)
),
status_bits=tuple(
BitDefinition(
key=f"status_{index:02d}",
title=f"Статус {index:02d}",
alarm=index in (7, 8),
)
for index in range(1, 9)
),
input_discretes=tuple(
BitDefinition(
key=f"input_{index:02d}",
title=f"Вход {index:02d}",
)
for index in range(1, 17)
),
output_discretes=tuple(
BitDefinition(
key=f"output_{index:02d}",
title=f"Выход {index:02d}",
writable=True,
)
for index in range(1, 17)
),
)
@dataclass
class SignalSnapshot:
"""@brief Атомарный снимок значений от одного момента времени.
Словари заменяются целиком при публикации, чтобы вкладки не видели смесь
данных от двух циклов опроса.
"""
timestamp_s: float = 0.0
numeric: dict[str, float] = field(default_factory=dict)
bits: dict[str, bool] = field(default_factory=dict)
class MockSignalSource:
"""@brief Детерминированный генератор данных без контроллера.
Не зависит от Qt и используется как переносимое ядро в host-тестах.
"""
def __init__(self, catalog: SignalCatalog) -> None:
self._catalog = catalog
self._outputs = {item.key: False for item in catalog.output_discretes}
def set_output(self, key: str, value: bool) -> None:
"""@brief Сохраняет состояние существующего template-выхода.
@param key Ключ из ``SignalCatalog.output_discretes``.
@param value Новое логическое состояние.
@raises KeyError Если ключ отсутствует в разрешённом наборе.
"""
if key not in self._outputs:
raise KeyError(f"Неизвестная управляющая дискрета: {key}")
self._outputs[key] = bool(value)
def snapshot(self, timestamp_s: float) -> SignalSnapshot:
"""@brief Генерирует согласованный снимок для заданного времени.
@param timestamp_s Монотонное время от запуска mock в секундах.
@return Новый независимый ``SignalSnapshot``.
"""
numeric: dict[str, float] = {}
bits: dict[str, bool] = {}
for index, item in enumerate(self._catalog.controller_fields):
numeric[item.key] = float((index + 1) * 10)
for index, item in enumerate(self._catalog.indicators):
center = item.maximum * 0.55
amplitude = item.maximum * 0.18
numeric[item.key] = center + amplitude * sin(timestamp_s * 0.7 + index)
for index, item in enumerate(self._catalog.status_bits):
bits[item.key] = int(timestamp_s + index) % (index + 2) == 0
for index, item in enumerate(self._catalog.input_discretes):
bits[item.key] = int(timestamp_s * 2 + index) % 5 < 2
bits.update(self._outputs)
return SignalSnapshot(timestamp_s=timestamp_s, numeric=numeric, bits=bits)