# Клиент A/B-загрузчика ProtoCAN с оконной передачей блоков по восемь байт. Ответы принимаются # только для целевого адреса и текущей сессии; продвижение по этапам зависит от подтверждения, # а не от факта отправки. """Legacy ProtoCAN Boot client ported from Gui_Android CanFirmwareProtocol.kt.""" from __future__ import annotations from dataclasses import dataclass import struct from .protocan import ProtoCanId from .transport import build_frame @dataclass(frozen=True) class CanBootTarget: device_type: int = 0 device: int = 13 product_type: int = 0x166 hardware_min: int = 0 hardware_max: int = 255 session_id: int = 1 memory: str = "eeprom" # Назначение выбирается явно; старые вызовы сохраняют EEPROM A/B и прежний wire-протокол. def __post_init__(self): if not 0 <= self.device_type <= 7 or not 0 <= self.device <= 15: raise ValueError("Адрес ProtoCAN: Device Type 0…7, Device 0…15") if not 0 <= self.product_type <= 65535: raise ValueError("Product Type вне диапазона u16") if not 0 <= self.hardware_min <= self.hardware_max <= 255: raise ValueError("Неверный диапазон аппаратных ревизий") if not 1 <= self.session_id <= 255: raise ValueError("Session ID должен быть 1…255") if self.memory not in ("eeprom", "flash", "stm32") or self.memory == "flash" and self.product_type != 0x2833: # F28335 требует отдельного подтверждения выбора Flash; STM32 использует штатный односекционный поток. raise ValueError("Внутренняя Flash по CAN доступна только для F28335 / Product Type 0x2833") # Ошибка назначения обнаруживается до первого CAN-кадра. if self.memory == "stm32" and self.product_type != 0: # BOOT CANdle G474VET6 публикует Product Type 0 в своём аппаратном профиле. raise ValueError("CANdle G474VET6 требует Product Type 0") # Не допускает случайную передачу образа с ранее выбранным типом платы Бальзам. class CanBootTransfer: """Windowed 8-byte blocks; control 0x9, slots 0xA/0xB, status 0xC.""" def __init__(self, image, target: CanBootTarget): if not 0 < len(image.data) <= 512 * 1024: raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ") if target.product_type == 0x2833 and len(image.data) > 0x1FE00: raise ValueError("F28335 M95M02 A/B: образ не должен превышать 130560 байт") if not 0 <= image.version <= 0xFFFFFFFF: raise ValueError("Версия образа вне диапазона u32") if target.memory == "stm32": # Односекционная Flash G474 проверяется до первой команды ENTER_BOOT. from set_devices.mcu_profiles import CANDLE_G474_BOOT # Размер приложения и адрес векторов берутся из общего профиля платы. profile = CANDLE_G474_BOOT # Границы соответствуют 32 КиБ BOOT и 480 КиБ приложения STM32G474VET6. if not 8 <= len(image.data) <= profile["app_size"]: # Первые восемь байтов содержат SP и Reset_Handler; запись не должна выходить за Flash. raise ValueError("CANdle G474VET6: приложение должно занимать 8…491520 байт") # Ошибка обнаруживается до стирания целевого приложения. sp, pc = struct.unpack_from("> 8 == t.session_id and len(frame.data) == 8) # Окно содержит до 16 блоков. Последний неполный блок заполняется 0xFF, # но метаданные образа сохраняют исходную длину без этого дополнения. def _window(self): total = (len(self.image.data) + 7) // 8 self._window_end = min(total, (self.next_block // 16 + 1) * 16) # Ядро подтверждает абсолютные границы по 16 блоков; после потери повторяется только хвост текущего окна, чтобы его ACK не застал в очереди кадры следующего окна. return [self._frame(10 + self.slot, i, self.image.data[i * 8:(i + 1) * 8].ljust(8, b"\xff")) for i in range(self.next_block, self._window_end)] def handle_status(self, frame): if not self.accepts(frame): return [], "" command = ProtoCanId.parse(frame.can_id).body & 255 expected_command = {"enter": 2, "memory": 16, "image": 3, "compat": 4, "erase": 5, # Ответ выбора назначения принимается только на собственном этапе. "data": 0, "verify": 6, "commit": 7, "reboot": 9}.get(self.stage) # Позднее подтверждение предыдущего этапа нельзя применять к новому: # оно не продвигает автомат и не считается ошибкой текущего запроса. if command != expected_command: return [], "" status, slot, expected = struct.unpack_from(" limit: raise RuntimeError("ProtoCAN Boot: подтверждён непереданный блок") # Один и тот же ACK или SEQUENCE_ERROR может прийти от нескольких оставшихся кадров старого окна. # Повторная отправка на каждый такой ответ переполняет очередь MCU; при отсутствии продвижения # последнее окно повторяет транспорт по тайм-ауту, а подтверждённые ранее блоки не отправляются заново. if self.stage == "data" and expected <= self.next_block: # Сравнивает next_block для обоих статусов: OK и SEQUENCE_ERROR не должны повторно открывать одно окно. return [], "" # Оставляет текущие кадры и срок ожидания транспорта без изменений до нового прогресса или тайм-аута. self.next_block = expected if expected == total: self.stage = "verify" return self._control(6), "Проверка CRC32" self.stage = "data" return self._window(), "Передача блоков ProtoCAN Boot" if self.stage == "verify": self.stage = "commit" return self._control(7), "Запись и проверка внутренней Flash" if self.target.memory == "flash" else "Активация образа" # Для Flash COMMIT означает реальный вызов TI API и публикацию FAB1 последней стадией. if self.stage == "commit": self.stage = "reboot" return self._control(9), "Перезапуск прибора" self.stage, self.finished = "finished", True if self.target.memory == "stm32": # У односекционного STM32 нет EEPROM-кандидата и механизма пробного запуска A/B. return [], "Flash STM32 записана и проверена; прибор перезапускается" # Успех сообщается после ACK VERIFY, COMMIT и REBOOT обычного протокола. if self.target.memory == "flash": # Flash не выдаётся за EEPROM-кандидата, которому требуется trial/CONFIRM. return [], "Внутренняя Flash записана и проверена; прибор перезапускается" # Успех возможен только после итогового ACK COMMIT и ACK REBOOT. if self.target.product_type == 0x2833: return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск" return [], "Прошивка ProtoCAN Boot завершена" class CanBootServices: """Read-only passport queries and explicit session service commands. Replies use message D, body=(page<<8)|command. IDENTIFY/discovery reply body=1. All multibyte values are little endian; version is major:8/minor:8/patch:16. """ def __init__(self, target: CanBootTarget): self.target = target def request(self, command: int, page: int = 0): if command not in (1, 8, 11, 12, 13, 14, 15): raise ValueError("Unsupported service command") if not 0 <= page <= 255 or (page and command not in (14, 15)): raise ValueError("Invalid service page") t = self.target payload = bytes([page]) if command in (14, 15) else b"" return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, 9, t.session_id << 8 | command), payload, to_can=True) @staticmethod def discover(): return build_frame(ProtoCanId.build(1, 0, 7, 15, 13, 0), b"", to_can=True) def decode(self, frame, *, discovery=False): p = ProtoCanId.parse(frame.can_id) if (frame.to_can or not frame.ide or frame.rtr or frame.is_error or p.pm != 1 or p.msg_type not in (12, 13) or len(frame.data) != 8): return None if discovery: if p.msg_type != 13 or p.body != 1: return None elif (p.device_type, p.device) != (self.target.device_type, self.target.device): return None if p.msg_type == 12: if p.body >> 8 != self.target.session_id: return None status, slot, next_block, crc32 = struct.unpack("> 8 d = frame.data result = {"kind": "passport", "device_type": p.device_type, "device": p.device, "command": command, "page": page} if command == 1 and page == 0: product, hardware, protocol, version = struct.unpack("> 24, (version >> 16) & 255, version & 65535)) elif command == 12 and page == 0: result.update(protocol_version=d[0], state=d[1], active_slot=d[2], target_slot=d[3], session_id=d[4], slot_count=d[5], last_status=d[6]) elif command == 13 and page == 0: flags, capacity = struct.unpack("