111 lines
14 KiB
Python
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)) # Результат используется только в исходящем эмулируемом снимке.
|