Расширить DEIF, загрузку CAN и общую обработку сигналов

Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt.
Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor.
Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов.
Обновить справочники устройств, проекты CCS и регрессионные проверки.
This commit is contained in:
2026-10-24 15:40:51 +03:00
parent a5e46d4627
commit 41e24629ce
170 changed files with 43762 additions and 281 deletions
+37 -7
View File
@@ -20,6 +20,7 @@ class CanBootTarget:
hardware_min: int = 0
hardware_max: int = 255
session_id: int = 1
memory: str = "eeprom" # Назначение выбирается явно; старые вызовы сохраняют EEPROM A/B и прежний wire-протокол.
def __post_init__(self):
if not 0 <= self.device_type <= 7 or not 0 <= self.device <= 15:
@@ -30,6 +31,10 @@ class CanBootTarget:
raise ValueError("Неверный диапазон аппаратных ревизий")
if not 1 <= self.session_id <= 255:
raise ValueError("Session ID должен быть 1…255")
if self.memory not in ("eeprom", "flash", "stm32") or self.memory == "flash" and self.product_type != 0x2833: # F28335 требует отдельного подтверждения выбора Flash; STM32 использует штатный односекционный поток.
raise ValueError("Внутренняя Flash по CAN доступна только для F28335 / Product Type 0x2833") # Ошибка назначения обнаруживается до первого CAN-кадра.
if self.memory == "stm32" and self.product_type != 0: # BOOT CANdle G474VET6 публикует Product Type 0 в своём аппаратном профиле.
raise ValueError("CANdle G474VET6 требует Product Type 0") # Не допускает случайную передачу образа с ранее выбранным типом платы Бальзам.
class CanBootTransfer:
@@ -41,6 +46,19 @@ class CanBootTransfer:
raise ValueError("F28335 M95M02 A/B: образ не должен превышать 130560 байт")
if not 0 <= image.version <= 0xFFFFFFFF:
raise ValueError("Версия образа вне диапазона u32")
if target.memory == "stm32": # Односекционная Flash G474 проверяется до первой команды ENTER_BOOT.
from set_devices.mcu_profiles import CANDLE_G474_BOOT # Размер приложения и адрес векторов берутся из общего профиля платы.
profile = CANDLE_G474_BOOT # Границы соответствуют 32 КиБ BOOT и 480 КиБ приложения STM32G474VET6.
if not 8 <= len(image.data) <= profile["app_size"]: # Первые восемь байтов содержат SP и Reset_Handler; запись не должна выходить за Flash.
raise ValueError("CANdle G474VET6: приложение должно занимать 8…491520 байт") # Ошибка обнаруживается до стирания целевого приложения.
sp, pc = struct.unpack_from("<II", image.data) # Таблица векторов ARM Cortex-M хранит два 32-битных little-endian адреса.
if sp & 7 or not profile["stack_base"] < sp <= profile["stack_end"]: # MSP выровнен на восемь байтов и входит в те же 80 КиБ SRAM, которые разрешает валидатор штатного BOOT CANdle.
raise ValueError("CANdle G474VET6: неверный начальный указатель стека") # Случайный BIN не рассматривается как приложение ARM.
if not pc & 1 or not profile["app_base"] <= (pc & ~1) < profile["app_base"] + len(image.data): # Thumb-вектор должен указывать внутрь приложения, слинкованного для 0x08008000.
raise ValueError("CANdle G474VET6: нужен образ приложения для 0x08008000") # BOOT-образ 0x08000000 и приложение USB BOOT RB не принимаются.
if target.memory == "flash": # Flash принимает тот же подписанный SCI8, что существующий RS Flash, с защищённым сектором A.
from .f28335_flash import parse_sci8 # Повторно использует общий валидатор FAB1, адресов и CRC без отдельной реализации в клиенте.
parse_sci8(image.data) # Обычный RAM BIN и образ с аппаратным boot отвергаются до входа в загрузчик.
self.image, self.target = image, target
self.stage = "enter"
self.next_block = 0
@@ -50,6 +68,8 @@ class CanBootTransfer:
@property
def response_timeout_ms(self):
if self.target.memory == "flash" and self.stage == "commit": # TI erase/program/verify выполняются синхронно после полной проверки staging.
return 180000 # Даёт до 180 секунд на штатную запись и итоговую CRC, как действующий RS Flash-клиент.
return 15000 if self.stage in ("enter", "verify", "commit") else 1500
@property
@@ -80,7 +100,7 @@ class CanBootTransfer:
# но метаданные образа сохраняют исходную длину без этого дополнения.
def _window(self):
total = (len(self.image.data) + 7) // 8
self._window_end = min(total, self.next_block + 16)
self._window_end = min(total, (self.next_block // 16 + 1) * 16) # Ядро подтверждает абсолютные границы по 16 блоков; после потери повторяется только хвост текущего окна, чтобы его ACK не застал в очереди кадры следующего окна.
return [self._frame(10 + self.slot, i, self.image.data[i * 8:(i + 1) * 8].ljust(8, b"\xff"))
for i in range(self.next_block, self._window_end)]
@@ -88,7 +108,7 @@ class CanBootTransfer:
if not self.accepts(frame):
return [], ""
command = ProtoCanId.parse(frame.can_id).body & 255
expected_command = {"enter": 2, "image": 3, "compat": 4, "erase": 5,
expected_command = {"enter": 2, "memory": 16, "image": 3, "compat": 4, "erase": 5, # Ответ выбора назначения принимается только на собственном этапе.
"data": 0, "verify": 6, "commit": 7, "reboot": 9}.get(self.stage)
# Позднее подтверждение предыдущего этапа нельзя применять к новому:
# оно не продвигает автомат и не считается ошибкой текущего запроса.
@@ -99,7 +119,10 @@ class CanBootTransfer:
return [], "Загрузчик занят"
if status not in (0, 8) or (status == 8 and self.stage != "data"):
raise RuntimeError(f"ProtoCAN Boot: ошибка 0x{status:02X}")
if self.stage == "enter":
if self.stage == "enter" and self.target.memory == "flash": # Старый загрузчик должен явно подтвердить поддержку новой цели до передачи образа.
self.stage = "memory" # Сначала требует ACK SELECT_MEMORY, затем начнёт обычные метаданные.
return self._control(16, b"\x01"), "Выбор внутренней Flash F28335" # DLC=1 и значение 1 означают внутреннюю Flash; значение 0 зарезервировано за штатной EEPROM.
if self.stage in ("enter", "memory"): # EEPROM сохраняет прежний переход, Flash переходит только после подтверждённого выбора памяти.
self.stage = "image"
return self._control(3, struct.pack("<II", len(self.image.data), self.image.crc32)), "Передача метаданных"
if self.stage == "image":
@@ -110,7 +133,7 @@ class CanBootTransfer:
if slot not in (0, 1):
raise RuntimeError("ProtoCAN Boot: неверный слот")
self.slot, self.stage = slot, "erase"
return self._control(5), "Стирание неактивного слота"
return self._control(5), ("Подготовка временного слота" if self.target.memory == "flash" else "Стирание приложения STM32" if self.target.memory == "stm32" else "Стирание неактивного слота") # F28335 готовит staging, STM32 стирает единственный слот, EEPROM сохраняет штатное поведение A/B.
if self.stage in ("erase", "data"):
total = (len(self.image.data) + 7) // 8
# The final index of a 512 KiB image wraps the u16 status field.
@@ -119,8 +142,11 @@ class CanBootTransfer:
limit = total if self.stage == "erase" else self._window_end
if expected > limit:
raise RuntimeError("ProtoCAN Boot: подтверждён непереданный блок")
if self.stage == "data" and status == 0 and expected < self.next_block:
return [], ""
# Один и тот же ACK или SEQUENCE_ERROR может прийти от нескольких оставшихся кадров старого окна.
# Повторная отправка на каждый такой ответ переполняет очередь MCU; при отсутствии продвижения
# последнее окно повторяет транспорт по тайм-ауту, а подтверждённые ранее блоки не отправляются заново.
if self.stage == "data" and expected <= self.next_block: # Сравнивает next_block для обоих статусов: OK и SEQUENCE_ERROR не должны повторно открывать одно окно.
return [], "" # Оставляет текущие кадры и срок ожидания транспорта без изменений до нового прогресса или тайм-аута.
self.next_block = expected
if expected == total:
self.stage = "verify"
@@ -129,11 +155,15 @@ class CanBootTransfer:
return self._window(), "Передача блоков ProtoCAN Boot"
if self.stage == "verify":
self.stage = "commit"
return self._control(7), "Активация образа"
return self._control(7), "Запись и проверка внутренней Flash" if self.target.memory == "flash" else "Активация образа" # Для Flash COMMIT означает реальный вызов TI API и публикацию FAB1 последней стадией.
if self.stage == "commit":
self.stage = "reboot"
return self._control(9), "Перезапуск прибора"
self.stage, self.finished = "finished", True
if self.target.memory == "stm32": # У односекционного STM32 нет EEPROM-кандидата и механизма пробного запуска A/B.
return [], "Flash STM32 записана и проверена; прибор перезапускается" # Успех сообщается после ACK VERIFY, COMMIT и REBOOT обычного протокола.
if self.target.memory == "flash": # Flash не выдаётся за EEPROM-кандидата, которому требуется trial/CONFIRM.
return [], "Внутренняя Flash записана и проверена; прибор перезапускается" # Успех возможен только после итогового ACK COMMIT и ACK REBOOT.
if self.target.product_type == 0x2833:
return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск"
return [], "Прошивка ProtoCAN Boot завершена"
+99
View File
@@ -0,0 +1,99 @@
import base64 # Декодирует загружаемые секции из официального XML TI OFD, сохраняя адреса LOAD и RUN.
from pathlib import Path # Сохраняет изменения только в явно переданном результате текущей сборки.
import struct # Упаковывает адресуемые 16-битные слова C28x младшей октетой вперёд.
import subprocess # Вызывает локальный инспектор объектных файлов без доступа к JTAG.
import xml.etree.ElementTree as ET # Разбирает фактические секции COFF независимо от соседней карты линкера.
import zlib # CRC32 IEEE совпадает с проверкой постоянного загрузчика и учитывает незаписанные слова 0xFFFF.
APP_START = 0x300000 # Сектор H начинает область приложения; аппаратный boot остаётся в секторе A.
HEADER_WORDS = 16 # Заголовок версии 1 занимает 16 слов C28x, то есть 32 физических октета.
APP_DATA = APP_START + HEADER_WORDS # Исполняемые секции следуют после служебного заголовка приложения.
APP_STOP = 0x338000 # Исключённая верхняя граница совпадает с началом защищённого сектора A.
def descriptor(entry, loaded): # Создаёт проверяемое описание Flash-приложения без сектора постоянного загрузчика.
if not loaded or any(not APP_START <= address < APP_STOP for address in loaded): # Любое слово в секторе A, CSM, OTP или RAM делает образ непригодным для обновления приложения.
raise ValueError('Flash-приложение пересекает защищённый загрузчик или выходит за область 0x300000–0x337FFF') # Отказ происходит до стирания физической памяти.
end = max(loaded) + 1 # Верхняя граница включает все Flash LOAD-секции, в том числе копируемый в RAM код API.
if not APP_DATA <= entry < end or entry not in loaded: # Точка запуска обязана указывать на действительно присутствующее слово кода после заголовка.
raise ValueError('Точка входа Flash-приложения отсутствует в загружаемом коде') # Исключает пустой заголовок и устаревшую RAM-сборку.
payload = struct.pack('<' + 'H' * (end - APP_DATA), *(loaded.get(address, 0xFFFF) for address in range(APP_DATA, end))) # Пробелы между секциями должны оставаться стёртыми; порядок октетов соответствует C28x.
crc = zlib.crc32(payload) # Контрольная сумма защищает весь непрерывный диапазон приложения, включая промежутки.
values = (0x4146, 0x3142, 1, HEADER_WORDS, entry & 65535, entry >> 16, APP_DATA & 65535, APP_DATA >> 16, end & 65535, end >> 16, crc & 65535, crc >> 16, (~entry) & 65535, ((~entry) >> 16) & 65535, 0x5AA5, 0xA55A) # Первые четыре физических октета образуют ASCII FAB1; далее следуют версия, размер, адреса, CRC, инверсия entry и маркеры завершения.
return struct.pack('<16H', *values) # Передаёт ровно 32 октета без платформенного выравнивания структуры.
def validate_words(entry, loaded): # Одинаково проверяет содержимое JTAG COFF и транспортной таблицы RS Flash.
if any(APP_START + index not in loaded for index in range(HEADER_WORDS)): # Все слова заголовка должны присутствовать явно, а не получаться заполнением 0xFFFF.
raise ValueError('Нет полного заголовка Flash-приложения FAB1; требуется новая сборка под постоянный загрузчик') # Старый образ с codestart по 0x33FFF6 не допускается.
actual = struct.pack('<16H', *(loaded[APP_START + index] for index in range(HEADER_WORDS))) # Получает описание именно из передаваемых данных.
if actual != descriptor(entry, loaded): # Проверяет CRC, границы, обе половины entry и маркеры одной независимой реконструкцией.
raise ValueError('Заголовок или CRC32 Flash-приложения FAB1 не соответствует данным') # Повреждение нельзя скрыть корректным именем файла или внешним паспортом.
def parse_sci8(data): # Разбирает только приложение ниже сектора A, не разрешая аппаратный переход ROM.
if len(data) < 24 or len(data) % 2 or data[:2] != b'\xaa\x08': # SCI8 имеет ключ 0x08AA, 22-октетный заголовок и чётный терминатор.
raise ValueError('Неверный заголовок или размер Flash SCI8') # Обычный бинарный дамп и COFF не являются таблицей адресных блоков TI.
def address(offset): # Адрес TI состоит из старшего 16-битного слова, затем младшего; каждое слово little endian.
high, low = struct.unpack_from('<HH', data, offset) # Читает две половины без зависимости от архитектуры Python.
return (high << 16) | low # Возвращает адрес в словах C28x, а не байтовое смещение.
entry, offset, loaded, blocks = address(18), 22, {}, [] # Хранит точку запуска, позицию таблицы и неперекрывающиеся диапазоны.
while offset + 2 <= len(data): # Длина блока должна присутствовать полностью даже для завершающего нуля.
count, = struct.unpack_from('<H', data, offset) # Размер полезных данных выражен 16-битными словами.
offset += 2 # Переходит к адресу блока либо к концу таблицы.
if not count: # Нулевой размер завершает адресную таблицу без последующего поля адреса.
if offset != len(data): # Дополнительные байты после терминатора недопустимы даже при правильной CRC полезной части.
raise ValueError('Хвост после терминатора Flash SCI8') # Не принимает склеенные или частично заменённые файлы.
validate_words(entry, loaded) # До передачи проверяет CRC и отсутствие слов постоянного загрузчика.
return entry, loaded, blocks # Вызывающий использует проверенные блоки для записи либо сравнения с COFF.
if offset + 4 + 2 * count > len(data): # Адрес и все слова должны помещаться в текущем файле.
raise ValueError('Обрезанный блок Flash SCI8') # Не читает случайные байты за границей изображения.
start = address(offset) # Получает абсолютный адрес назначения очередного блока.
offset += 4 # Пропускает две половины абсолютного адреса.
if not APP_START <= start < start + count <= APP_STOP: # Сектор A и весь аппаратный boot исключены независимо от имени секции.
raise ValueError(f'Запрещённый Flash-диапазон 0x{start:06X}, {count} слов: загрузчик защищён') # Показывает точное пересечение до начала обновления.
values = struct.unpack_from('<' + 'H' * count, data, offset) # Читает только полностью проверенный блок.
if any(start + index in loaded for index in range(count)): # Дублирование адреса могло бы скрыть подмену заголовка поздним блоком.
raise ValueError('Перекрывающиеся блоки Flash SCI8') # Запрещает неоднозначный порядок программирования.
loaded.update((start + index, value) for index, value in enumerate(values)) # Сохраняет каждое слово по его физическому LOAD-адресу.
blocks.append((start, data[offset:offset + count * 2])) # Сохраняет исходные октеты без изменения порядка секций.
offset += count * 2 # Переходит к размеру следующего блока.
raise ValueError('Нет терминатора Flash SCI8') # Обрезанный файл не считается полным приложением.
def coff_sections(document): # Извлекает адреса и точные октеты из результата TI ofd2000 -x.
coff = document.find('.//ti_coff') # Рабочая прошивка F28335 использует COFF ABI.
if coff is None or coff.findtext('file_header/target_id') != '0x9d' or coff.findtext('file_header/exec') != 'true': # Проверяет процессор и признак связанного исполняемого файла.
raise ValueError('Требуется исполняемый COFF C2000') # Объектные модули и файлы другой архитектуры не программируются.
entry = int(coff.findtext('optional_file_header/entry_point', '0'), 0) # Entry задаёт запуск приложения через общий Flash boot.
sections, loaded = {}, {} # Отдельно хранит XML полей секций и адресное пространство программируемых слов.
for section in coff.findall('section'): # Рассматривает все секции, включая LOAD для функций, исполняемых из RAM.
sections[section.findtext('name')] = section # Служебный заголовок находится по стабильному имени .flash_app.
raw, start = section.findtext('raw_data'), int(section.findtext('virtual_addr', '0'), 0) # OFD называет virtual адресом LOAD, physical — RUN.
if raw is None or section.findtext('copy') == 'true': # Только явно незагружаемые COPY/отладочные секции и секции без данных исключаются из программирования.
continue # Их наличие не расширяет область разрешённого программирования.
payload = base64.b64decode(raw) # Получает точные октеты вместо доверия к соседней MAP.
if len(payload) != int(section.findtext('raw_data_size', '0'), 0) or len(payload) % 2 or not APP_START <= start < start + len(payload) // 2 <= APP_STOP: # Любая загружаемая секция обязана целиком находиться ниже A; RAM, OTP, CSM и несогласованный размер запрещены.
raise ValueError('COFF содержит запрещённую Flash-секцию: ' + str(section.findtext('name'))) # Старый самостоятельный codestart будет отклонён.
for index, value in enumerate(struct.unpack('<' + 'H' * (len(payload) // 2), payload)): # Восстанавливает 16-битное представление каждого слова C28x.
if start + index in loaded: # Две секции не должны программировать одно физическое слово.
raise ValueError('Перекрывающиеся LOAD-секции COFF') # Сохраняет единственность заголовка и однозначную контрольную сумму.
loaded[start + index] = value # Собирает ожидаемый Flash-образ для проверки CRC и конверсии SCI8.
return entry, loaded, sections # Метаданные нужны как подписанию, так и независимой проверке готового OUT.
def seal_coff(image, inspector): # Заполняет только заранее выделенный linker заголовок после успешной линковки.
image = Path(image) # Принимает путь текущей сборки без зависимости от рабочего каталога.
document = ET.fromstring(subprocess.check_output([str(inspector), '-x', str(image)])) # Доверяет размещению секций, подтверждённому официальным TI-инспектором.
entry, loaded, sections = coff_sections(document) # Отсекает любой LOAD в сектор A до изменения двоичного файла.
section = sections.get('.flash_app') # Секция размером 16 слов резервируется linker и не меняет адресов остальной программы.
if section is None or int(section.findtext('virtual_addr', '0'), 0) != APP_START or int(section.findtext('raw_data_size', '0'), 0) != HEADER_WORDS * 2: # Нельзя добавлять заголовок поверх машинного кода.
raise ValueError('Linker должен выделить .flash_app из 16 слов по адресу 0x300000') # Ошибка требует пересборки вместо небезопасного смещения программы.
offset = int(section.findtext('file_offsets/raw_data_ptr', '0'), 0) # OFD указывает точное файловое смещение содержимого секции COFF.
raw, signed = bytearray(image.read_bytes()), descriptor(entry, loaded) # Считает CRC уже окончательно связанных Flash LOAD-секций.
if offset <= 0 or offset + len(signed) > len(raw): # Проверяет размер файла до адресной записи заголовка.
raise ValueError('Неверное смещение заголовка COFF') # Повреждённые метаданные не могут изменить постороннюю часть OUT.
raw[offset:offset + len(signed)] = signed # Меняет только зарезервированные 32 октета, не трогая символы, адреса и отладочную информацию.
image.write_bytes(raw) # Сохраняет подписанный результат до конверсии в SCI8 и создания внешнего SHA-256.
loaded.update((APP_START + index, value) for index, value in enumerate(struct.unpack('<16H', signed))) # Приводит адресную модель к записанному заголовку.
validate_words(entry, loaded) # Независимо проверяет только что сформированный результат.
+11
View File
@@ -136,14 +136,25 @@ MSGTYPE_RU = {
#: но не присваивает приборам имён.
DEVICE_TYPE_NAMES = {
0x0: "Верхний уровень",
0x1: "PERIPH 0–15", # Excel «Интерфейс протокола», C6/F6: первый тип содержит младшие 16 номеров периферии.
0x2: "PERIPH 16–31", # Excel C7/F7: второй тип продолжает нумерацию, сохраняя четырёхбитный Device ID.
0x6: "Логические анализаторы",
}
DEVICE_ADDRESS_NAMES = {
(0x1, 0x5): "УМП1", # Excel B50/C50/F50 закрепляет отдельный адрес первого модуля нагрузки.
(0x1, 0x6): "УМП2", # Excel B51/C51/F51 отделяет второй модуль от первого и от задатчика.
(0x1, 0x8): "Задатчик", # Excel B53/C53/F53 задаёт адрес задатчика без сдвига старого CAN Mode на единицу.
(0x6, 0xE): "Altera Logic",
(0x7, 0xD): "configurator",
}
def periph_address(periph_id: int) -> tuple[int, int]: # Возвращает адрес ProtoCAN для номера из 32 строк обновлённой таблицы PERIPH.
if not 0 <= periph_id < 32: # Число 32 задаёт полный диапазон двух типов по 16 экземпляров; за его пределами адрес не назначается.
raise ValueError("ID PERIPH должен быть в диапазоне 0…31") # Запрещает незаметное оборачивание ошибочного номера на другую плату.
return 1 + periph_id // 16, periph_id % 16 # Старшая группа выбирает Type 1/2, младшие четыре бита — Device ID 0…15.
SETTINGS_RESULT_RU = {
SettingsResult.OK: "успех",
SettingsResult.INVALID_DLC: "неверный DLC",
+28
View File
@@ -0,0 +1,28 @@
import ctypes # Привязка передаёт готовые слова общему C-кодеку без повторения wire-формата на Python.
from .native import _load_library # Стандартный поиск сохраняет существующий механизм доставки SETProtocol.
class StmInvertorCodec: # Один экземпляр владеет только сигнатурами функций общей библиотеки.
def __init__(self, library=None): # Явная DLL позволяет новой панели не заменять библиотеку работающего приложения.
self.lib = library if library is not None else _load_library() # Нет Python fallback при отсутствии нового C-кодека.
self.lib.set_invertor_gas_limit.argtypes = [ctypes.c_uint] # Тип сообщения передаётся без вычисления геометрии карты в Python.
self.lib.set_invertor_gas_limit.restype = ctypes.c_uint # Границы определяются профилем C из Templates.
self.lib.set_invertor_gas_build.argtypes = [ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint16), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint8)] # Типы совпадают с публичным C ABI общего адаптера.
self.lib.set_invertor_gas_build.restype = ctypes.c_int # C возвращает фактический DLC либо отрицательную ошибку.
self.lib.set_invertor_gas_decode.argtypes = [ctypes.c_uint32, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint16)] # Декодер проверяет ID, тип, направление, длину и маску.
self.lib.set_invertor_gas_decode.restype = ctypes.c_int # Число принятых wire-слов приходит только из C.
def limit(self, message_type): # Интерфейс не хранит отдельную копию константы 250 или 750.
return int(self.lib.set_invertor_gas_limit(message_type)) # Передаём актуальную границу виджету адреса.
def build(self, message_type, address, words=()): # Пустой набор слов означает чтение DLC0 в обоих протоколах.
if not 0 <= address <= 0xFFFF or len(words) > 4 or any(not 0 <= word <= 0xFFFF for word in words): # Проверяем представимость Python-чисел перед сужением до типов ABI.
raise ValueError('Адрес или значение вне диапазона ABI') # Не допускаем тихого усечения и циклического переноса ctypes.
source = (ctypes.c_uint16 * len(words))(*words) if words else None # Значения остаются словами, endian выбирает общий кодек.
identifier = ctypes.c_uint32(); data = (ctypes.c_uint8 * 8)() # Локальные буферы имеют размеры Classic CAN и не переживают вызов.
length = self.lib.set_invertor_gas_build(message_type, address, source, len(words), ctypes.byref(identifier), data) # Ни CAN ID, ни маска не строятся здесь вручную.
if length < 0: # C отклоняет неверную геометрию, длину блока и маску.
raise ValueError('GAS: неверный адрес, длина блока или маска Word0') # Пользователь видит причину отказа до передачи на шину.
return identifier.value, bytes(data[:length]) # Транспорт получает полностью готовые ID и октеты.
def decode(self, identifier, flags, payload): # Входной кадр проверяется до публикации значений интерфейсу.
if not 0 <= identifier <= 0x1FFFFFFF or len(payload) > 8: # Не допускаем сужения большого CAN ID и переполнения временного буфера.
return None # Кадры другого транспорта не считаются ответом инвертора.
data = (ctypes.c_uint8 * 8)(*payload); metadata = (ctypes.c_uint * 2)(); words = (ctypes.c_uint16 * 4)() # ABI работает с фиксированными массивами без указателей на объекты Python.
count = self.lib.set_invertor_gas_decode(identifier, flags, data, len(payload), metadata, words) # Wire-разбор остаётся единственной C-реализацией.
return (metadata[0], metadata[1], tuple(words[:count])) if count > 0 else None # Недопустимый кадр не изменяет отображённые значения.