Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
125
python/set_devices/firmware.py
Normal file
125
python/set_devices/firmware.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""@file firmware.py
|
||||
@brief Проверка образов и переносимая машина блочной передачи прошивки.
|
||||
|
||||
Реализует загрузку и валидацию образов прошивки из различных форматов файлов,
|
||||
проверку целостности данных, и логику передачи образов на устройство.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from set_devices.protocol import crc32_ieee
|
||||
|
||||
|
||||
class FirmwareImageError(ValueError):
|
||||
"""@brief Файл нельзя безопасно представить как непрерывный бинарный образ."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FirmwareImage:
|
||||
"""@brief Загруженный образ прошивки с метаданными.
|
||||
|
||||
@param path Путь к исходному файлу образа
|
||||
@param data Бинарные данные образа
|
||||
@param base_address Адрес начала загрузки (для Intel HEX формата)
|
||||
@param version Номер версии прошивки
|
||||
"""
|
||||
path: Path
|
||||
data: bytes
|
||||
base_address: int = 0
|
||||
version: int = 0
|
||||
|
||||
@property
|
||||
def crc32(self) -> int:
|
||||
return crc32_ieee(self.data)
|
||||
|
||||
@property
|
||||
def sha256(self) -> str:
|
||||
return hashlib.sha256(self.data).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path, maximum_size: int = 2 * 1024 * 1024) -> "FirmwareImage":
|
||||
file_path = Path(path)
|
||||
raw = file_path.read_bytes()
|
||||
if file_path.suffix.lower() == ".hex":
|
||||
data, base = _parse_intel_hex(raw.decode("ascii"))
|
||||
elif file_path.suffix.lower() == ".bin":
|
||||
data, base = raw, 0
|
||||
else:
|
||||
raise FirmwareImageError("Поддерживаются только .bin и Intel HEX")
|
||||
if not data:
|
||||
raise FirmwareImageError("Образ пуст")
|
||||
if len(data) > maximum_size:
|
||||
raise FirmwareImageError(f"Размер образа превышает {maximum_size} байт")
|
||||
return cls(file_path, data, base)
|
||||
|
||||
|
||||
def _parse_intel_hex(text: str) -> tuple[bytes, int]:
|
||||
memory: dict[int, int] = {}
|
||||
upper = 0
|
||||
eof = False
|
||||
for line_number, source in enumerate(text.splitlines(), 1):
|
||||
line = source.strip()
|
||||
if not line:
|
||||
continue
|
||||
if not line.startswith(":"):
|
||||
raise FirmwareImageError(f"Intel HEX: строка {line_number} без ':'")
|
||||
try:
|
||||
record = bytes.fromhex(line[1:])
|
||||
except ValueError as error:
|
||||
raise FirmwareImageError(f"Intel HEX: неверный HEX в строке {line_number}") from error
|
||||
if len(record) < 5 or len(record) != record[0] + 5 or sum(record) & 0xFF:
|
||||
raise FirmwareImageError(f"Intel HEX: неверная длина/CRC строки {line_number}")
|
||||
count = record[0]
|
||||
address = int.from_bytes(record[1:3], "big")
|
||||
kind = record[3]
|
||||
payload = record[4 : 4 + count]
|
||||
if kind == 0x00:
|
||||
absolute = upper + address
|
||||
for index, value in enumerate(payload):
|
||||
key = absolute + index
|
||||
if key in memory and memory[key] != value:
|
||||
raise FirmwareImageError("Intel HEX содержит конфликтующие диапазоны")
|
||||
memory[key] = value
|
||||
elif kind == 0x01:
|
||||
eof = True
|
||||
break
|
||||
elif kind == 0x04 and count == 2:
|
||||
upper = int.from_bytes(payload, "big") << 16
|
||||
elif kind == 0x02 and count == 2:
|
||||
upper = int.from_bytes(payload, "big") << 4
|
||||
elif kind not in (0x03, 0x05):
|
||||
raise FirmwareImageError(f"Intel HEX: тип записи {kind:#x} не поддержан")
|
||||
if not eof or not memory:
|
||||
raise FirmwareImageError("Intel HEX не содержит данных или EOF")
|
||||
start, end = min(memory), max(memory) + 1
|
||||
image = bytearray(b"\xFF" * (end - start))
|
||||
for address, value in memory.items():
|
||||
image[address - start] = value
|
||||
return bytes(image), start
|
||||
|
||||
|
||||
@dataclass
|
||||
class FirmwareTransfer:
|
||||
image: FirmwareImage
|
||||
block_size: int = 480
|
||||
offset: int = 0
|
||||
|
||||
def next_block(self) -> tuple[int, bytes] | None:
|
||||
if self.offset >= len(self.image.data):
|
||||
return None
|
||||
offset = self.offset
|
||||
return offset, self.image.data[offset : offset + self.block_size]
|
||||
|
||||
def acknowledge(self, next_offset: int) -> None:
|
||||
current = self.next_block()
|
||||
if current is None or next_offset != current[0] + len(current[1]):
|
||||
raise FirmwareImageError("МК подтвердил неожиданный offset")
|
||||
self.offset = next_offset
|
||||
|
||||
@property
|
||||
def percent(self) -> int:
|
||||
return int(self.offset * 100 / len(self.image.data))
|
||||
Reference in New Issue
Block a user