Files
templates/python/set_devices/firmware_identity.py

111 lines
5.6 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 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("Неизвестный формат паспорта")
if meta['product'] != expected_product or meta['target'] != expected_target:
raise ValueError(f"Паспорт предназначен для другого устройства (ожидается {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 отличаются."