Files
Balsam_167/tests/firmware_identity/run_test.py
Andrey fe95dbff3d Перенести паспорт и проверку прошивки из 167 в 166
Добавить неизменяемое окно 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 байт. Аппаратная проверка требует установки нового образа и перезапуска платы.
2026-10-16 12:05:44 +03:00

58 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Проверяет точечный перенос паспорта 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") # Подтверждает проверку выпуска и совместимость без физического обмена с платой.