Files
templates/python/protocan/uksvep.py
T

111 lines
14 KiB
Python

"""Тонкий Python-порт общей CAN-карты UKSVEP на STM32F103RC.""" # Форматы, GPIO и расчёт неисправностей принадлежат C99 templates.
from __future__ import annotations # Аннотации остаются совместимыми с исходниками Windows 7.
import ctypes as ct # Передаёт скаляры и буферы единственной нативной реализации.
from dataclasses import dataclass # Представляет готовые результаты C без протокольных вычислений.
from .native import get_native_protocol, NativeProtocolUnavailable # Использует штатный поиск и жизненный цикл общей DLL.
@dataclass(frozen=True) # Метаданные одинаковы для всех представлений GUI.
class Field: # Значения читаются из package.h-совместимой таблицы C.
address: int # Нулевой индекс регистра modbus[128].
maximum: int # Верхняя граница ввода в исходных единицах.
initial: int # Значение предпросмотра, которое ещё не принято от платы.
flags: int # 1 — параметр, 2 — команды, 4 — эмулируемое состояние.
name: str # Общая подпись UTF-8 из C-профиля.
@dataclass(frozen=True) # Пакет отделён от физического CAN-драйвера.
class Frame: # Готовые байты нельзя изменять через поля виджета после отправки.
can_id: int # Полный 29-битный ID без аппаратного бита IDE.
data: bytes # Восемь байтов payload, построенных C.
@dataclass(frozen=True) # Снимок содержит только реально присутствующие слова.
class Packet: # Источник пакета отделяет ответ ВЭП от команды управляющего узла.
from_device: bool # Истина означает ID данных ВЭП, а не собственное эхо команд.
values: tuple[tuple[int, int], ...] # Пары адрес/значение, разрешённые маской присутствия C.
class UksvepCan: # Каждая панель использует общее нативное ядро без собственного codec.
def __init__(self): # Привязывает только дополнительные экспорты профиля UKSVEP.
self.lib = get_native_protocol().lib # Загрузка DLL и общий ABI уже проверены штатным портом.
signatures = { # Типы описывают ABI, а не байтовый порядок wire-протокола.
"uksvep_can_id": ([ct.c_uint, ct.c_uint], ct.c_uint32), # Возвращает маршрут для физического адреса и роли.
"uksvep_can_encode": ([ct.c_uint, ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t, ct.POINTER(ct.c_uint8), ct.c_size_t], ct.c_size_t), # Кодирует до трёх слов в буфер caller.
"uksvep_can_decode": ([ct.c_uint, ct.c_uint32, ct.POINTER(ct.c_uint8), ct.c_size_t, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает плоский массив пар из C.
"uksvep_field_count": ([], ct.c_uint), # Позволяет получить все поля без таблицы дублей Python.
"uksvep_field_info": ([ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает четыре скаляра метаданных.
"uksvep_field_name": ([ct.c_uint], ct.c_char_p), # C владеет постоянной строкой UTF-8.
"uksvep_input_name": ([ct.c_uint], ct.c_char_p), # Назначения GPIO остаются общими с профилем.
"uksvep_command_name": ([ct.c_uint], ct.c_char_p), # Пустое имя обозначает резервный бит R127.
"uksvep_evaluate_inputs": ([ct.c_uint16, ct.c_uint16, ct.c_uint16, ct.POINTER(ct.c_uint16), ct.c_size_t], None), # Рассчитывает R1/R2 для эмуляции по алгоритму C.
} # Символы добавлены без изменения существующего ABI версии 1.
try: # Старая DLL должна давать понятную ошибку вместо тихого Python fallback.
for name, (arguments, result) in signatures.items(): # Каждый экспорт получает точную сигнатуру до первого вызова.
function = getattr(self.lib, name) # Отсутствие профиля в старой DLL обнаруживается здесь.
function.argtypes = arguments # ctypes не угадывает размер указателей и целых.
function.restype = result # Сохраняет 32-битные ID и указатели строк на 64-битном хосте.
except AttributeError as error: # Формат wire не реализуется заново в аварийной ветви.
raise NativeProtocolUnavailable("В общей DLL нет профиля UKSVEP. Пересоберите setprotocol.dll из templates.") from error # Поясняет требуемое действие пользователю.
@staticmethod # Валидация Python-аргумента предотвращает обрезание ctypes до C-типа.
def _device(value): # Адрес хранится целым числом из четырёх перемычек.
if not isinstance(value, int) or not 0 <= value <= 15: # Нельзя молча адресовать соседний узел по младшим битам.
raise ValueError("DeviceID ВЭП должен быть 0…15") # Ошибка возникает до передачи CAN.
return value # Валидный скаляр далее проверяется также C-профилем.
def can_id(self, device, from_device=False): # Доверяет вычисление маршрута общему C-профилю.
return int(self.lib.uksvep_can_id(self._device(device), bool(from_device))) # Python не повторяет базу и смещение идентификатора.
def fields(self, device=0): # Получает подтверждённые поля выбранного ВЭП.
device = self._device(device) # Проверяет адрес до создания начальных значений Mode.
result = [] # Список содержит только готовые метаданные ABI.
for index in range(self.lib.uksvep_field_count()): # Размер карты также принадлежит C.
raw = (ct.c_uint16 * 4)() # Caller владеет явно ограниченным выходным буфером.
if not self.lib.uksvep_field_info(index, device, raw, len(raw)): # Проверяет результат вызова вместо чтения частично готового массива.
raise ValueError("Не удалось получить карту UKSVEP") # Ошибка не заменяется выдуманными defaults.
name = self.lib.uksvep_field_name(index).decode("utf-8") # Декодирует только строковое представление общей подписи.
result.append(Field(*map(int, raw), name)) # Не меняет единицы и адреса, возвращённые C.
return tuple(result) # Карта не изменяется виджетами во время работы.
def names(self, kind): # Читает общие назначения входов либо команд.
function = self.lib.uksvep_input_name if kind == "inputs" else self.lib.uksvep_command_name # Выбор относится к категории отображения, а не к codec.
return tuple(function(bit).decode("utf-8") for bit in range(16)) # Резервные позиции остаются пустыми строками.
def frames(self, device, address, values, from_device=False): # Делит длинный снимок на штатные вызовы трёхсловного C-кодировщика.
device = self._device(device) # Неверный адрес не может быть обрезан ctypes.
values = tuple(values) # Фиксирует входной снимок до первой физической записи.
if not values or not isinstance(address, int) or not 0 <= address < 128 or len(values) > 128 - address: # Проверяет Python-диапазон до преобразования unsigned.
raise ValueError("Выберите непустой диапазон R0…R127") # Ошибка обнаруживается до формирования первого пакета.
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Не обрезает отрицательные или переполненные слова.
raise ValueError("Значения ВЭП должны быть uint16: 0…65535") # Поле пользователя не должно превращаться в другое число на проводе.
result = [] # Все байты создаются до передачи, чтобы не отправить половину неверного снимка.
for offset in range(0, len(values), 3): # Ограничение трёх слов задано существующим историческим форматом.
part = values[offset:offset + 3] # UI-порт только разбивает контейнер, не строит маску/endian.
source = (ct.c_uint16 * len(part))(*part) # Преобразует значения в явно типизированный вход C.
target = (ct.c_uint8 * 8)() # Payload classic CAN ограничен восемью байтами.
size = self.lib.uksvep_can_encode(device, bool(from_device), address + offset, source, len(part), target, len(target)) # Все поля wire заполняет C99.
if size != 8: # Валидный UKSVEP кадр обязан иметь полный DLC.
raise ValueError("C-профиль отклонил регистры UKSVEP") # Ошибка не скрывается частичной передачей.
result.append(Frame(self.can_id(device, from_device), bytes(target))) # Возвращает готовый транспортно-независимый кадр.
return tuple(result) # Физический writer принадлежит существующему BusTab.
def decode(self, device, can_id, data): # Вызывает общий decoder с ограниченными буферами.
device = self._device(device) # Проверяет адрес до входа в ctypes.
if not isinstance(can_id, int) or not 0 <= can_id <= 0x1FFFFFFF: # Исключает знаковое переполнение и биты вне extended ID.
return None # Такое событие не принадлежит допустимому CAN-профилю.
data = bytes(data) # Фиксирует входной payload на время вызова C.
source = (ct.c_uint8 * len(data)).from_buffer_copy(data) # Caller владеет памятью до завершения синхронного разбора.
target = (ct.c_uint16 * 8)() # Восемь скаляров вмещают направление и три адресные пары.
count = self.lib.uksvep_can_decode(device, can_id, source, len(data), target, len(target)) # C выбирает ID, DLC, маску и границы карты.
if count < 0: # Повреждённый пакет выбранного ВЭП не считается чужим.
raise ValueError("Повреждённый CAN-кадр UKSVEP: DLC, маска или адрес") # UI может увеличить счётчик отказов RX.
if count == 0: # Пакет другого адреса не меняет снимок.
return None # Общая шина продолжает обрабатывать его независимо.
pairs = tuple((int(target[2 + index * 2]), int(target[3 + index * 2])) for index in range(count)) # Переводит плоский ABI в готовые пары без повторного endian-разбора.
return Packet(bool(target[0]), pairs) # Направление не позволяет выдать эхо TX за ответ платы.
def evaluate(self, inputs, alarm_mask, error_mask): # Предоставляет явный расчёт состояния эмулируемого ВЭП.
values = (inputs, alarm_mask, error_mask) # Передаёт только исходные слова C-алгоритму ReadEnteres.
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Защищает параметры от неявного uint16 обрезания.
raise ValueError("Входы и маски должны быть uint16") # Неверный ввод не превращается в правдоподобную аварию.
result = (ct.c_uint16 * 2)() # Буфер содержит R1 и R2, вычисленные C.
self.lib.uksvep_evaluate_inputs(*values, result, len(result)) # Python не повторяет особые случаи заряд/разряд/24 В.
return tuple(map(int, result)) # Результат используется только в исходящем эмулируемом снимке.