feat(protocan-boot): Добавь порты MCU и сервисы загрузки
Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент. Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
This commit is contained in:
+101
-3
@@ -37,6 +37,8 @@ class CanBootTransfer:
|
||||
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")
|
||||
self.image, self.target = image, target
|
||||
@@ -46,6 +48,10 @@ class CanBootTransfer:
|
||||
self.finished = False
|
||||
self._window_end = 0
|
||||
|
||||
@property
|
||||
def response_timeout_ms(self):
|
||||
return 15000 if self.stage in ("enter", "verify", "commit") else 1500
|
||||
|
||||
@property
|
||||
def percent(self):
|
||||
return min(100, self.next_block * 8 * 100 // len(self.image.data))
|
||||
@@ -74,7 +80,7 @@ class CanBootTransfer:
|
||||
# но метаданные образа сохраняют исходную длину без этого дополнения.
|
||||
def _window(self):
|
||||
total = (len(self.image.data) + 7) // 8
|
||||
self._window_end = min(total, self.next_block + 16)
|
||||
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)]
|
||||
|
||||
@@ -113,8 +119,11 @@ class CanBootTransfer:
|
||||
limit = total if self.stage == "erase" else self._window_end
|
||||
if expected > limit:
|
||||
raise RuntimeError("ProtoCAN Boot: подтверждён непереданный блок")
|
||||
if self.stage == "data" and status == 0 and expected < self.next_block:
|
||||
return [], ""
|
||||
# Один и тот же 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"
|
||||
@@ -128,4 +137,93 @@ class CanBootTransfer:
|
||||
self.stage = "reboot"
|
||||
return self._control(9), "Перезапуск прибора"
|
||||
self.stage, self.finished = "finished", True
|
||||
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("<BBHI", frame.data)
|
||||
return {"kind": "status", "device_type": p.device_type, "device": p.device,
|
||||
"command": p.body & 255, "status": status, "target_slot": slot,
|
||||
"next_block": next_block, "crc32": crc32}
|
||||
command, page = p.body & 255, p.body >> 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("<HBBI", d)
|
||||
result.update(product_type=product, hardware_revision=hardware, protocol_version=protocol,
|
||||
firmware_version=version, version=(version >> 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("<II", d)
|
||||
result.update(capabilities=flags, slot_capacity=capacity)
|
||||
elif command == 14:
|
||||
result["name_fragment"] = d.split(b"\0", 1)[0].decode("ascii", errors="replace")
|
||||
elif command == 15:
|
||||
if page == 0:
|
||||
size, page_size, mode, running = struct.unpack("<IHBB", d)
|
||||
result.update(memory_bytes=size, page_bytes=page_size, bootloader=bool(mode), running_slot=running)
|
||||
elif page == 1:
|
||||
result["slot_offsets"] = struct.unpack("<II", d)
|
||||
elif page == 2:
|
||||
active, pending, attempted, healthy, timeout = struct.unpack("<BBBBI", d)
|
||||
result.update(active_slot=active, pending_slot=pending, trial_attempted=bool(attempted),
|
||||
storage_healthy=bool(healthy), session_timeout_ms=timeout)
|
||||
elif page == 3:
|
||||
result["parameters_offset"], result["parameters_bytes"] = struct.unpack("<II", d)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
class CanBootPingExchange:
|
||||
"""PING adapter with the same request/feed shape as the SETProtocol client."""
|
||||
def __init__(self, target: CanBootTarget):
|
||||
self.services = CanBootServices(target)
|
||||
|
||||
def request(self):
|
||||
return [self.services.request(12)]
|
||||
|
||||
def feed(self, frame):
|
||||
reply = self.services.decode(frame)
|
||||
return reply if reply and reply["kind"] == "passport" and reply["command"] == 12 else None
|
||||
|
||||
Reference in New Issue
Block a user