Files
templates/python/set_devices/firmware_identity.py
T
Andrey 41e24629ce Расширить DEIF, загрузку CAN и общую обработку сигналов
Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt.
Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor.
Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов.
Обновить справочники устройств, проекты CCS и регрессионные проверки.
2026-10-24 15:40:51 +03:00

133 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.
"""Firmware-info v1 and the adjacent release passport (no device I/O)."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
from protocan.periph28335 import with_crc
MAX_IMAGE_SIZE = 199993
REGISTER_ADDRESS = 0x1200
REGISTER_COUNT = 12
def read_request(controller: int) -> bytes:
if not 1 <= controller <= 255:
raise ValueError("Для чтения версии нужен адрес контроллера 1..255")
return with_crc(bytes((controller, 3)) + REGISTER_ADDRESS.to_bytes(2, 'big')
+ REGISTER_COUNT.to_bytes(2, 'big'))
@dataclass(frozen=True)
class FirmwareIdentity:
version: tuple[int, int, int]
built_at: datetime
build_id: str
def __post_init__(self):
if (len(self.version) != 3 or any(type(v) is not int or not 0 <= v <= limit
for v, limit in zip(self.version, (255, 255, 999)))
or not 2000 <= self.built_at.year <= 2199
or self.built_at.utcoffset() is None
or self.built_at.microsecond
or len(self.build_id) != 8
or any(not 32 <= ord(c) <= 126 for c in self.build_id)):
raise ValueError("Некорректные данные версии прошивки")
def describe(self):
version = '.'.join(map(str, self.version))
return f"{version} · {self.built_at:%Y-%m-%d %H:%M:%S} UTC · Build ID {self.build_id}"
def decode_identity(words):
if len(words) != REGISTER_COUNT or words[0] != 1:
raise ValueError("Неизвестный формат версии (требуется firmware-info v1)")
if any(type(w) is not int or not 0 <= w <= 65535 for w in words):
raise ValueError("Некорректные регистры версии")
try:
stamp = datetime(words[4], words[5] >> 8, words[5] & 255,
words[6] >> 8, words[6] & 255, words[7], tzinfo=timezone.utc)
build_id = bytes(b for w in words[8:] for b in (w >> 8, w & 255)).decode('ascii')
return FirmwareIdentity(tuple(words[1:4]), stamp, build_id)
except (ValueError, UnicodeError) as error:
raise ValueError("Некорректные данные версии: " + str(error)) from error
def validate_image_device(meta, expected_product, expected_target="TMS320F28335"): # Сопоставляет назначение в паспорте с настройкой загрузчика без обращения к подключённой плате.
product, target = meta.get('product', 'не указана'), meta.get('target', 'не указан') # Отсутствующее назначение тоже даёт понятный отказ, включая Flash RS, где KeyError не является пользовательской ошибкой.
if product == expected_product and target == expected_target: # Совпадение обоих полей сохраняет прежнее разрешение на дальнейшие проверки BIN.
return # Не заменяет проверку размера, SHA-256 и формата, которую вызывающий код выполняет отдельно.
models = {'Balsam-166-PM35': 'Бальзам-166 ПМ35', 'Balsam-167-PM35': 'Бальзам-167 ПМ35'} # Человекочитаемые названия соответствуют двум моделям из списка «Модель платы».
selected = models.get(str(expected_product), str(expected_product)) # Для других устройств сохраняет исходное обозначение, не выдавая неизвестную модель за 166 или 167.
actual = models.get(str(product), str(product)) # Показывает назначение из паспорта; ошибочный тип поля тоже не мешает вывести причину отказа.
differences = [] # Собирает только действительно различающиеся признаки, чтобы общий процессор не выглядел причиной отказа.
if product != expected_product: # Различие моделей проверяется независимо от процессора, который может совпадать у разных плат.
differences.append('модели платы') # Указывает на расхождение обозначений 166/167 даже при одинаковом TMS320F28335.
if target != expected_target: # Отдельно выявляет несовместимый процессор без ошибочного обвинения модели платы.
differences.append('процессора') # Дополняет причину вторым признаком, только если его значение отличается.
raise ValueError( # Блокирует загрузку с подробным объяснением; метаданные не доказывают аппаратное назначение содержимого BIN.
'Назначение прошивки не совпадает с выбранным устройством. Загрузка заблокирована.\n' # Сразу сообщает результат проверки и его влияние на загрузку.
f'Выбрано в программе: {selected} ({expected_product}); процессор {expected_target}.\n' # Связывает ожидаемые значения с пользовательской настройкой, а не с результатом опроса платы.
f'В паспорте файла: {actual} ({product}); процессор {target}.\n' # Показывает фактические значения полей паспорта рядом с ожидаемыми.
f'Причина: несовпадение {" и ".join(differences)}. Модель подключённой платы этой проверкой не определялась.\n' # Разделяет установленное расхождение метаданных и неизвестное состояние оборудования.
f'Для платы {selected} выберите BIN с паспортом для этой модели и процессора {expected_target}. ' # Предлагает комплект для нужной платы, сохраняя выбранную пользователем модель.
f'Если эта прошивка собрана для {selected}, проверьте назначение в настройках сборки и заново сформируйте выпуск с паспортом. ' # Объясняет исправление ошибочной маркировки сборки без подмены значений готового паспорта.
'Поле «Модель платы» меняйте, только если в программе выбрана не ваша плата.' # Не предлагает выбирать 167 ради обхода сообщения при реальной плате 166.
) # Завершает единое сообщение, одинаковое для EEPROM BIN и Flash RS BIN.
def inspect_image(path: Path, data: bytes | None = None, *, expected_eeprom=None,
expected_product="Balsam-167-PM35", expected_target="TMS320F28335",
max_image_size=MAX_IMAGE_SIZE):
"""Return None for legacy images; reject a present but invalid passport.
The caller may supply the exact bytes it will upload, avoiding a second
read between hash verification and transmission.
"""
if data is None:
with path.open('rb') as stream:
data = stream.read(max_image_size + 1)
if not 0 < len(data) <= max_image_size:
raise ValueError(f"Размер бинарного файла должен быть 1..{max_image_size} байт")
passport = Path(str(path) + '.manifest.json')
if not passport.exists():
return None
try:
with passport.open('r', encoding='utf-8-sig') as stream:
raw = stream.read(65537)
if len(raw) > 65536:
raise ValueError("Слишком большой паспорт прошивки")
meta = json.loads(raw)
if (meta['schema'] != 1 or meta['identity_contract'] != 1
or meta['identity_register_count'] != REGISTER_COUNT):
raise ValueError("Неизвестный формат паспорта")
validate_image_device(meta, expected_product, expected_target) # При несовпадении показывает выбранные и паспортные модель/процессор с причиной отказа и способом исправления.
if (type(meta['size_bytes']) is not int or meta['size_bytes'] != len(data)
or meta['sha256'] != hashlib.sha256(data).hexdigest()):
raise ValueError("BIN не соответствует паспорту: размер или SHA-256 не совпадает")
if expected_eeprom is not None and meta.get('eeprom') is not None:
if meta['eeprom'] != expected_eeprom:
raise ValueError(f"BIN для {meta['eeprom']}, а выбрана EEPROM {expected_eeprom}")
stamp = datetime.fromisoformat(meta['built_at_utc'].replace('Z', '+00:00'))
if stamp.utcoffset() is None or stamp.utcoffset().total_seconds() != 0:
raise ValueError("Дата паспорта должна быть в UTC")
return FirmwareIdentity(tuple(meta['version']), stamp, meta['build_id'])
except (KeyError, TypeError, ValueError, AttributeError) as error:
raise ValueError("Ошибка паспорта: " + str(error)) from error
def compare_identity(device, image):
if device is None or image is None:
return "Сравнение недоступно: нужны версия с платы и проверенный паспорт BIN."
if device == image:
return "Версия, дата сборки и Build ID совпадают. Содержимое памяти платы этим не проверяется."
if image.version > device.version:
return "Выбранная версия новее установленной."
if image.version < device.version:
return "Выбранная версия старее установленной (понижение версии)."
return "Номер версии совпадает, но дата сборки или Build ID отличаются."