Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt. Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor. Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов. Обновить справочники устройств, проекты CCS и регрессионные проверки.
130 lines
18 KiB
Python
130 lines
18 KiB
Python
"""Независимая от GUI Python-модель индикатора поверх общего C99-ядра DEIF.""" # Настройки и транспортные имена остаются в порту, wire-правила находятся в C.
|
|
import ctypes as C # Обмен с ABI использует фиксированные массивы и caller-owned память.
|
|
from dataclasses import dataclass # Привязка является простой моделью пользовательской настройки.
|
|
from time import monotonic # Порт передаёт часы ядру явно в миллисекундах.
|
|
from .deif_native import get_deif_native, Frame # Единственная реализация кодека и автомата загружается без Qt.
|
|
|
|
FIELDS = ("power", "rpm", "angle", "power_percent", "rpm_percent", "angle_setpoint") # Порядок полей публичного ABI C; это имена модели, не wire-селекторы.
|
|
STATUS = {1: "ниже диапазона", 2: "выше диапазона", 3: "ошибка данных"} # Локализованные подписи готовых статусов C.
|
|
|
|
|
|
@dataclass # Привязки можно сохранять в существующих пользовательских профилях JSON.
|
|
class Binding: # Четыре HEX-байта и коэффициент поступают из настроек либо C-каталога.
|
|
selector: str = "" # Пустая строка означает явно отключённое поле.
|
|
scale: float = 1.0 # Коэффициент применяется только в общем C-ядре.
|
|
|
|
|
|
def _milliseconds(now): # Перевод единиц системных часов относится к платформенному порту.
|
|
return max(0, round((monotonic() if now is None else now) * 1000)) # ABI использует целые монотонные миллисекунды для одинаковых границ на всех платформах.
|
|
|
|
|
|
def validate_deif_transmit(frame): # Сохраняет прежний публичный Python API ограниченного маршрута MASTER.
|
|
if not get_deif_native().validate(frame.can_id, bytes(frame.data), len(frame.data), int(bool(frame.ide)) | (int(bool(frame.rtr)) << 1), 0): # Проверка адреса, объектов и размеров полностью выполняется в C.
|
|
raise ValueError("DEIF: разрешены только данные, яркость, Color и heartbeat; команды настройки через Node-ID запрещены") # Текст остаётся локализованной ответственностью приложения.
|
|
|
|
|
|
def brightness_frame(node_id, group, percent, can_id=None, mode="sam"): # Готовит один кадр без отправки или изменения прибора.
|
|
return get_deif_native().make_frame(6, node_id, percent, 0 if can_id is None else can_id, mode, group) # Поле ABI 6 обозначает яркость, исходный COB-ID и U8 определяет C.
|
|
|
|
|
|
def colour_palette_frame(node_id, group, palette, can_id=None, mode="sam"): # Готовит независимый от яркости кадр палитры.
|
|
return get_deif_native().make_frame(7, node_id, palette, 0 if can_id is None else can_id, mode, group) # Поле ABI 7 обозначает Color; исходный COB-ID и кодирование Day/Night/Dusk находятся в C.
|
|
|
|
|
|
class DeifDisplay: # Пассивная модель пригодна для Desktop, CLI и тестов без Qt.
|
|
services = False # Наблюдатель не объявляет себя узлом CAN и никогда не выдаёт heartbeat.
|
|
expiry_ms = 5000 # Прежний режим наблюдения скрывает показания после пяти секунд без обновления.
|
|
|
|
def __init__(self, source="DevBoard", can_id=0x201, power_unit="RAW", device_node=None, bindings=None, xdi_net=True, self_start=True, dam_pdo=2, dimmer_group=1, source_node=1): # Дополнительные параметры передаются серверным портом, прежние профили остаются совместимыми.
|
|
self.source, self.can_id, self.power_unit, self.device_node = source, can_id, power_unit, device_node # Только имя транспорта и UI-единица остаются вне C.
|
|
self.xdi_net, self.self_start, self.dam_pdo, self.dimmer_group, self._source_node = xdi_net, self_start, dam_pdo, dimmer_group, source_node # Параметры профиля передаются C без копии правил протокола.
|
|
self._api, self._context, self._route, self._bindings_signature = get_deif_native(), None, None, None # Один контекст на прибор исключает смешивание показаний MASTER и SLAVE.
|
|
self.bindings = {} # Контейнер поддерживает прямое редактирование существующим диалогом привязок.
|
|
self._ensure_context() # Исходные селекторы и коэффициенты создаются единственным C-каталогом.
|
|
if bindings is None: # Новый профиль получает штатные привязки, включая азимут.
|
|
for index, key in enumerate(FIELDS[:5]): # Отдельное задание угла автоматически производно от привязки азимута в C.
|
|
selector, factor = (C.c_uint8 * 4)(), C.c_double() # Порт принимает готовые значения без разбора endian.
|
|
self._api.get_binding(self._context, index, selector, C.byref(factor)) # Настройки по умолчанию читаются из C-профиля.
|
|
self.bindings[key] = Binding(bytes(selector).hex(" ").upper(), factor.value) # HEX нужен только редактору настроек.
|
|
else: # Существующая библиотека сохраняет пользовательские объекты и масштабы.
|
|
self.bindings = {key: Binding(item.selector, item.scale) for key, item in bindings.items()} # Словари двух приборов не должны делить изменяемые Binding.
|
|
|
|
def _ensure_context(self): # Изменение адреса требует нового C-контекста, а смена имени транспорта — нет.
|
|
route = (self.can_id, self.device_node, self.xdi_net, self.self_start, self.dam_pdo, self.dimmer_group, self.services, self._source_node) # Изменение источника также требует согласованной переинициализации.
|
|
if route == self._route: # Повторное чтение снимка не переинициализирует данные.
|
|
return # Сохраняет сроки и очередь работающего узла.
|
|
size = self._api.state_size() # Размер private C-структуры сообщает библиотека.
|
|
self._context = (C.c_uint64 * ((size + 7) // 8))() # uint64 обеспечивает необходимое выравнивание на Windows и ARM.
|
|
source = self._source_node # Источник профиля не вычисляется из имени GUI или физического Node-ID SLAVE.
|
|
if not self._api.init(self._context, size, self.device_node or 0, source, bool(self.xdi_net), bool(self.self_start), self.dam_pdo, self.dimmer_group, bool(self.services), self.can_id): # C проверяет reserved Node, RPDO и сетевые группы.
|
|
raise ValueError("DEIF: неверный профиль SLAVE, Node-ID или RPDO") # Частично инициализированный узел не используется.
|
|
self._route, self._bindings_signature = route, None # Привязки повторно синхронизируются после изменения профиля.
|
|
|
|
def _sync_bindings(self): # Прямые изменения словаря UI передаются в C только при фактическом изменении настройки.
|
|
self._ensure_context() # Проверяет, не был ли изменён физический адрес либо CAN ID наблюдения.
|
|
signature = tuple((key, binding.selector, binding.scale) for key, binding in self.bindings.items()) # Это сравнение настроек, а не интерпретация wire-формата.
|
|
if signature == self._bindings_signature: # Частые тики и приём кадров не должны сбрасывать значения.
|
|
return # Состояние C остаётся прежним.
|
|
for index, key in enumerate(FIELDS): # Порядок соответствует публичным field IDs ядра.
|
|
if index == 5 and key not in self.bindings: # Производное задание азимута настраивает C при изменении поля angle.
|
|
continue # Не отключает автоматически созданную независимую привязку 0x07.
|
|
binding = self.bindings.get(key, Binding()) # Отсутствующее поле явно отключается.
|
|
selector = bytes.fromhex(binding.selector) if binding.selector else None # HEX является форматом хранения настроек, а не CAN-декодером.
|
|
if selector is not None and len(selector) != 4: # Порт проверяет размер буфера до передачи указателя C.
|
|
raise ValueError("Селектор DEIF должен содержать четыре байта") # Некорректная пользовательская настройка не создаёт чтение за пределами буфера.
|
|
if not self._api.bind(self._context, index, selector, binding.scale): # Интерпретация селектора и применение масштаба находятся в C.
|
|
raise ValueError("DEIF: неверная привязка или масштаб") # Нечисловой коэффициент не скрывается как отсутствие пакета.
|
|
self._bindings_signature = signature # Последующая обработка кадра использует уже настроенный контекст.
|
|
|
|
def bind_default_angle(self): # Миграция старого профиля с пустым азимутом не требует знания адресов в GUI.
|
|
self._sync_bindings() # Источник и режим берутся из уже настроенных остальных полей.
|
|
selector, factor = (C.c_uint8 * 4)(), C.c_double() # C возвращает готовую библиотечную привязку.
|
|
if not self._api.default_angle(self._context, selector, C.byref(factor)): # Ни SAM/DAM, ни индекс не вычисляются в Python.
|
|
raise ValueError("Не удалось восстановить привязку азимута DEIF") # Ошибка миграции остаётся видимой.
|
|
self.bindings["angle"] = Binding(bytes(selector).hex(" ").upper(), factor.value) # Обновляется только отсутствующая настройка азимута.
|
|
self._sync_bindings() # Новая привязка применяется сразу в текущем сеансе.
|
|
|
|
def reset(self): # Сохраняет API очистки показаний при смене пользовательского канала.
|
|
self._sync_bindings() # Настройки должны соответствовать очищаемому контексту.
|
|
self._api.reset_values(self._context) # Очистка выполняется общим ядром.
|
|
|
|
def accept(self, source, identifier, data, extended=False, rtr=False, now=None): # Порт связывает транспортное имя с независимой моделью прибора.
|
|
if source != self.source or not 0 <= identifier <= 0x1FFFFFFF or len(data) > 8: # Имя канала относится к приложению, границы — к безопасному ABI.
|
|
return False # Кадры другого транспорта не обновляют C-контекст.
|
|
self._sync_bindings() # Диалог мог изменить привязки с момента предыдущего кадра.
|
|
accepted = bool(self._api.accept(self._context, identifier, bytes(data), len(data), int(bool(extended)) | (int(bool(rtr)) << 1), _milliseconds(now))) # Единственное место протокольной обработки находится в C.
|
|
return accepted and (self.services or not self._api.inspect_frame(identifier, data, extended, rtr)[1][7]) # Пассивный API сохраняет прежний признак отказа только для текущего ошибочного I16, не для других полей.
|
|
|
|
def _snapshots(self, now=None): # Снимок переводит готовые C-поля в контейнеры Python без повторения алгоритма.
|
|
self._sync_bindings() # Требуется также после прямого редактирования масштаба в настройках.
|
|
result, moment = {}, _milliseconds(now) # Все поля одного снимка читаются в один момент времени.
|
|
for index, key in enumerate(FIELDS): # Порядок ABI не зависит от порядка ключей пользовательского словаря.
|
|
value, metadata = C.c_double(), (C.c_int64 * 4)() # C выдаёт масштабированное значение и флаги seen/valid/stale.
|
|
self._api.read(self._context, index, moment, self.expiry_ms, C.byref(value), metadata) # Свежесть и служебные статусы вычисляются ядром.
|
|
result[key] = (value.value, metadata[0] / 1000, metadata[2], metadata[3]) # Деление переводит только единицы системных часов порта.
|
|
return result # Снимок не раскрывает внутренний layout C.
|
|
|
|
@property # Совместимый read-only снимок прежнего словаря показаний.
|
|
def values(self): # Значения остаются в C, Python не хранит вторую копию состояния.
|
|
return {key: item[0] for key, item in self._snapshots().items() if item[3] & 2} # Готовый valid-флаг исключает служебные I16.
|
|
|
|
@property # Временные отметки нужны существующей отрисовке и диагностике.
|
|
def received(self): # Даже invalid является принятым обновлением объекта, а не отсутствием связи.
|
|
return {key: item[1] for key, item in self._snapshots().items() if item[3] & 1} # Решение о наличии сообщения уже принято C-ядром.
|
|
|
|
def value(self, key, now=None): # Сохраняет интерфейс получения одного свежего показания.
|
|
item = self._snapshots(now).get(key) # Масштаб и свежесть получены из общего C-снимка.
|
|
return item[0] if item and item[3] & 2 and not item[3] & 4 else None # Python лишь отображает valid/stale флаги ABI.
|
|
|
|
def angle_selection(self, now=None): # Одно правило азимута используется и пассивным монитором, и активным SLAVE.
|
|
self._sync_bindings() # Профиль мог быть загружен из старого пользовательского файла.
|
|
value = C.c_double() # Владение выходным скаляром остаётся у порта.
|
|
kind = self._api.angle(self._context, _milliseconds(now), self.expiry_ms, C.byref(value)) # C выбирает фактический угол либо принятое сетевое задание.
|
|
return (value.value if kind else None, kind == 2) # Второй элемент нужен только для подписи «задание» в GUI.
|
|
|
|
@property # Список источников нужен только заголовку прибора, не алгоритму приёма.
|
|
def sam_sources(self): # UI получает готовые адреса без масок и смещений селектора.
|
|
from .deif_controls import selector_info # Разбор адреса селектора делегируется общему C-порту.
|
|
descriptors = [selector_info(binding.selector) for binding in self.bindings.values() if binding.selector] # Сохраняет индивидуальные привязки каждого поля.
|
|
return sorted({item.node for item in descriptors if item is not None and not item.dam}) # DAM-получатель не выдаётся за источник SAM.
|