Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt. Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor. Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов. Обновить справочники устройств, проекты CCS и регрессионные проверки.
133 lines
11 KiB
Python
133 lines
11 KiB
Python
"""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 отличаются."
|