Добавить неизменяемое окно firmware-info v1 0x1200 из 12 слов для FC03 и служебного CAN, запретить запись. Сохранить текущую технологическую карту и логгер v4 с записью 12 слов. Подключить общий генератор версии 1.0.0 для Balsam-166-PM35, prebuild CCS и локальный выпуск BIN с manifest и SHA-256. Проверять соответствие встроенных слов манифесту до публикации; старый OUT без паспорта отклонять. Проверка: реальные обработчики RS/CAN и CRC под GCC O3, прежний тест логгера, анализатор установленного SetGUI, полная сборка TI C2000 22.6.0.LTS. BIN 30248 байт при границе параметров 32508 байт. Аппаратная проверка требует установки нового образа и перезапуска платы.
58 lines
11 KiB
Python
58 lines
11 KiB
Python
"""Проверяет точечный перенос паспорта 167 в действующие обработчики 166.""" # Использует реальные функции, общий статический порт и независимый эталон регистра.
|
||
import importlib.util # Загружает проверку встроенного паспорта без запуска конверсии или сборки.
|
||
import json # Формирует независимый тестовый manifest.
|
||
import os # Подключает runtime установленного GCC только дочерним процессам.
|
||
from pathlib import Path # Находит production-исходники и общую библиотеку относительно теста.
|
||
import re # Извлекает функции Modbus без копирования их реализации в тест.
|
||
import struct # Создаёт минимальный SCI8 с известными загрузочными словами.
|
||
import subprocess # Компилирует настоящие обработчики под оптимизацией O3.
|
||
import sys # Подключает общий анализатор паспорта, который использует SetGUI.
|
||
import tempfile # Изолирует временные заголовки и исполняемые файлы от выпуска прошивки.
|
||
ROOT = Path(__file__).resolve().parents[2] # Корень 166 содержит проверяемый message.c.
|
||
TEMPLATES = ROOT.parent / "newProject/templates" # Использует ту же общую зависимость, что проект 167 и текущая сборка.
|
||
def load(name, path): # Импортирует проектный скрипт без его CLI и побочных эффектов postbuild.
|
||
spec = importlib.util.spec_from_file_location(name, path); module = importlib.util.module_from_spec(spec) # Создаёт независимый модуль с точным путём источника.
|
||
spec.loader.exec_module(module); return module # Возвращает реальные функции проверки версии и SCI8.
|
||
builder = load("balsam166_build", ROOT / "UKSSTMS320F28335/build_identity.py") # Проверяет именно выпускной валидатор проекта.
|
||
prepare = load("balsam166_prepare", ROOT / "UKSSTMS320F28335/prepare_identity.py") # Проверяет тот же парсер SemVer, что выполняет CCS prebuild.
|
||
source = re.sub(r"/\*.*?\*/|//[^\n]*", "", (ROOT / "Source/Internal/message.c").read_text(encoding="utf-8"), flags=re.S) # Исключает скобки из комментариев при выделении определений.
|
||
def function(name): # Извлекает рабочую функцию с произвольной вложенностью условий.
|
||
match = re.search(r"(?:static\s+)?void\s+" + name + r"\([^;{}]*\)\s*\{", source); assert match, name # Не принимает вызов или прототип за определение.
|
||
end, depth = match.end(), 1 # Начинает подсчёт после открывающей скобки функции.
|
||
while depth: depth += (source[end] == "{") - (source[end] == "}"); end += 1 # Находит внешнюю закрывающую скобку настоящего алгоритма.
|
||
return source[match.start():end] # Сохраняет все операторы производственного обработчика.
|
||
model = Path(__file__).with_name("port_test.c").read_text(encoding="utf-8") # Окружение содержит только UART/IRQ и наблюдаемый backend логгера.
|
||
model = model.replace("/* PRODUCTION_MODBUS */", "\n".join(function(name) for name in ("modbus_exception", "ReceiveCommandModbus3", "ReceiveCommandModbus6"))) # Подставляет реальный код, который попадёт в прошивку.
|
||
compiler = os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe") # Сохраняет возможность запускать тест доступным C99-компилятором.
|
||
environment = dict(os.environ); environment["PATH"] = str(Path(compiler).parent) + os.pathsep + environment.get("PATH", "") # Не меняет окружение CCS или приложения.
|
||
with tempfile.TemporaryDirectory(prefix="firmware166-test-") as directory: # Удаляет только собственные временные данные теста.
|
||
temporary = Path(directory); cfile = temporary / "port.c"; exe = temporary / "port.exe" # Разделяет C-модель и результат исполнения.
|
||
values = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH", "FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"), (1, 0, 0, 2026, 10, 16, 8, 52, 22))) # Детерминированный паспорт независим от времени запуска теста.
|
||
values.update({f"FI_ID_{i}": ord(char) for i, char in enumerate("1234abcd")}) # Даёт известную последовательность ASCII-слов для проверки порядка октетов.
|
||
(temporary / "firmware_identity.h").write_text("\n".join(f"#define {name} {value}U /* Проверочная константа паспорта v1. */" for name, value in values.items()), encoding="utf-8") # Сохраняет fixture заголовка в требуемой кодировке.
|
||
cfile.write_text(model, encoding="utf-8") # Сохраняет извлечённые русские исходники без изменения production-файла.
|
||
command = [compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Werror", "-I" + str(temporary), "-I" + str(ROOT / "Source/Internal/Include"), "-I" + str(TEMPLATES / "c/firmware-info/include"), str(cfile), str(ROOT / "Source/Internal/crc16.c"), "-o", str(exe)] # Проверяет типы и реальные CRC вместе с обработчиками RS/CAN.
|
||
subprocess.run(command, env=environment, check=True); subprocess.run([str(exe)], env=environment, check=True, timeout=10) # Ошибка типов, поведения или зависание делают тест неуспешным.
|
||
metadata = {"schema": 1, "product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 0], "build_id": "1234abcd", "built_at_utc": "2026-10-16T08:52:22+00:00", "identity_contract": 1, "identity_register_count": 12} # Фиксирует весь контракт manifest, который должен принять существующий GUI.
|
||
words = [1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364] # Независимый эталон совпадает с показанным протоколом, а не берётся из C-инициализатора.
|
||
data = b"\xaa\x08" + bytes(20) + struct.pack("<HHH12HH", 12, 0, 0x8000, *words, 0) # Создаёт корректный SCI8 с одним 12-словным блоком и терминатором.
|
||
image = temporary / "firmware.bin"; image.write_bytes(data); builder.verify_embedded(image, metadata) # Выпуск должен принять образ с достоверно совпавшим паспортом.
|
||
for broken in (data[:-2], data + b"\0\0", data[:28] + bytes(24) + data[52:], bytes(32509)): # Проверяет усечённый файл, хвост, чужие слова паспорта и пересечение параметров EEPROM.
|
||
image.write_bytes(broken) # Подменяет только временный проверяемый образ.
|
||
try: builder.verify_embedded(image, metadata) # Настоящий postbuild-валидатор должен отклонить каждый дефект.
|
||
except (ValueError, struct.error): pass # Ожидаемый отказ подтверждает защиту выпуска.
|
||
else: raise AssertionError("Invalid or stale SCI8 was accepted") # Не допускает фиктивного manifest поверх старой прошивки.
|
||
image.write_bytes(data); settings = temporary / "metadata.json"; settings.write_text(json.dumps(metadata), encoding="utf-8") # Восстанавливает корректный временный выпуск для проверки GUI.
|
||
sys.path.insert(0, str(TEMPLATES / "c/firmware-info/tools")); from identity import seal # Использует общий формат манифеста, применяемый в 167.
|
||
seal(image, settings) # Подписывает точные байты тестового BIN общим алгоритмом SHA-256.
|
||
native = ROOT.parent / "newProject/SETGUI/src/gui_desktop/native/setprotocol.dll" # Существующий GUI содержит ту же native-библиотеку CRC, которую импортирует анализатор паспорта.
|
||
if native.is_file(): os.environ.setdefault("SETPROTOCOL_LIBRARY", str(native)) # Сохраняет явно заданный путь пользователя и подключает установленный GUI без изменения файлов зависимости.
|
||
gui_python = ROOT.parent / "newProject/SETGUI/third_party/templates/python" # Проверяет фактическую vendored-копию анализатора текущего SetGUI, которая может отличаться от основной ветки templates.
|
||
sys.path.insert(0, str(gui_python if (gui_python / "set_devices/firmware_identity.py").is_file() else TEMPLATES / "python")); from set_devices.firmware_identity import inspect_image, decode_identity # Без установленного GUI использует тот же общий контракт библиотеки.
|
||
identity = inspect_image(image, expected_product="Balsam-166-PM35"); assert identity == decode_identity(words) # Версия из BIN и версия из ответа платы должны сравниваться без адаптации GUI.
|
||
try: inspect_image(image, expected_product="Balsam-167-PM35") # Перенос проверки не должен разрешать выдавать прошивку 166 за 167.
|
||
except ValueError: pass # Существующая защита GUI отличает product двух плат.
|
||
else: raise AssertionError("166 firmware accepted as 167") # Ошибка product делает перенос небезопасным для выбора модели.
|
||
assert prepare.read_version(ROOT / "UKSSTMS320F28335/firmware_version.h") == [1, 0, 0] # Проверяет единственный источник начальной версии текущего продукта.
|
||
print("PASS: embedded identity, stale/truncated/oversize rejection, exact manifest and unchanged SetGUI decoder") # Подтверждает проверку выпуска и совместимость без физического обмена с платой.
|