# ProtoCAN Boot Protocol
Статус: **Draft**
Версия протокола: **1.0**
Совместимость: **classic CAN 2.0B, Extended ID, DLC 0…8**
Реализация: [`templates/c/protocan-boot`](../../../c/protocan-boot)
## Назначение
Сервис обновляет адресованный прибор по CAN и поддерживает два логических
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
проверяется и атомарно назначается кандидатом на запуск.
## Карта сообщений
| `MsgType` | Имя | `MsgBody` | Payload |
|---:|---|---|---|
| `0x9` | `BOOT_CONTROL` | `SessionID[15:8] \| Command[7:0]` | параметры команды |
| `0xA` | `BOOT_DATA_A` | `BlockIndex[15:0]` | 8 байт слота A |
| `0xB` | `BOOT_DATA_B` | `BlockIndex[15:0]` | 8 байт слота B |
| `0xC` | `BOOT_STATUS` | `SessionID[15:8] \| Command[7:0]` | статус и прогресс |
| `0xD` | `BOOT_DISCOVERY` | подтип ответа | идентификация |
Все команды записи адресуются конкретному `DeviceType/DeviceID` и имеют
`Route=0`. Ответы сохраняют адрес прибора и имеют `Route=1`.
## Адресация образа
`MsgBody` кадра данных — номер 8-байтового блока:
```c
offset = (uint32_t)BlockIndex * 8U;
address = SLOT_X_BASE + offset;
```
```text
512 КиБ = 524 288 байт
524 288 / 8 = 65 536 блоков
BlockIndex = 0x0000…0xFFFF
```
| `BlockIndex` | Смещение | Диапазон байтов |
|---:|---:|---:|
| `0x0000` | `0x00000` | `0x00000…0x00007` |
| `0x0001` | `0x00008` | `0x00008…0x0000F` |
| `0xFFFF` | `0x7FFF8` | `0x7FFF8…0x7FFFF` |
`0x80000` является первой позицией за границей слота. Последний кадр
дополняется `0xFF`, но CRC32 вычисляется только по `ImageSize` байтам.
## Команды `BOOT_CONTROL`
| Код | Команда | DLC | Payload | Допустимое состояние |
|---:|---|---:|---|---|
| `0x01` | `IDENTIFY` | 0 | отсутствует | любое |
| `0x02` | `ENTER_BOOT` | 0 | отсутствует | любое; `SessionID != 0` |
| `0x03` | `BEGIN_IMAGE` | 8 | размер и CRC32 | metadata |
| `0x04` | `BEGIN_COMPAT` | 8 | совместимость и версия | metadata |
| `0x05` | `ERASE` | 0 | отсутствует | ready-to-erase |
| `0x06` | `VERIFY` | 0 | отсутствует | образ получен |
| `0x07` | `COMMIT` | 0 | отсутствует | verified |
| `0x08` | `CONFIRM` | 0 | отсутствует | запущенное приложение |
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
### `BEGIN_IMAGE`
```text
DATA[0..3] ImageSize, uint32 little-endian
DATA[4..7] ImageCRC32, uint32 little-endian
```
### `BEGIN_COMPAT`
```text
DATA[0..1] ProductType, uint16 little-endian
DATA[2] HardwareRevisionMin
DATA[3] HardwareRevisionMax
DATA[4..7] FirmwareVersion, uint32 little-endian
```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
## `BOOT_STATUS`
```text
MsgBody[15..8] SessionID
MsgBody[7..0] команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
DATA[2..3] NextBlock, uint16 little-endian
DATA[4..7] RunningCRC32, uint32 little-endian
```
| Код | Статус | Повтор допустим |
|---:|---|---|
| `0x00` | `OK` | — |
| `0x01` | `BUSY` | да, после задержки |
| `0x02` | `INVALID_COMMAND` | после исправления |
| `0x03` | `WRONG_DEVICE` | нет для этого образа |
| `0x04` | `WRONG_HARDWARE` | нет для этого образа |
| `0x05` | `INVALID_SIZE` | нет для этого образа |
| `0x06` | `CRC_ERROR` | новая передача |
| `0x07` | `FLASH_ERROR` | зависит от платформы |
| `0x08` | `SEQUENCE_ERROR` | да, с `NextBlock` |
| `0x09` | `SIGNATURE_ERROR` | нет |
| `0x0A` | `SESSION_ERROR` | открыть новую сессию |
| `0x0B` | `VOLTAGE_ERROR` | да после нормализации питания |
| `0x0C` | `INVALID_STATE` | выполнить правильный переход |
## State machine
```text
IDLE
└─ ENTER_BOOT ─> METADATA
├─ BEGIN_IMAGE
└─ BEGIN_COMPAT
│
v
READY_TO_ERASE
│ ERASE
v
RECEIVING
│ VERIFY
v
VERIFIED
│ COMMIT
v
PENDING + REBOOT
│ CONFIRM
v
CONFIRMED
```
Ошибка Flash, CRC, совместимости или подписи переводит сессию в `FAILED`.
Новая `ENTER_BOOT` создаёт чистую сессию. `ABORT` прекращает текущую передачу,
не активируя частично записанный слот.
## Надёжность и повторы
- Блоки передаются строго по возрастанию `BlockIndex`.
- Дубликат или пропуск возвращает `SEQUENCE_ERROR` и ожидаемый `NextBlock`.
- Базовый режим подтверждает каждый блок.
- Рабочий режим может подтверждать окно из 16 блоков.
- После потери связи `QUERY_PROGRESS` возвращает следующий ожидаемый блок,
пока состояние загрузчика сохранено.
- Для продолжения после перезагрузки порт должен сохранять session metadata
и восстановить её при инициализации; ядро версии 1.0 само это не делает.
## Безопасность и A/B-обновление
```text
active=A -> target=B -> verify -> pending=B
active=B -> target=A -> verify -> pending=A
```
CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
дополнительно проверить подпись контейнера, границы вектора, совместимость и
anti-rollback. Bootloader не обновляется командами `BOOT_DATA_A/B`.
Boot metadata должна атомарно хранить:
- активный слот;
- pending-слот;
- подтверждение запуска;
- число неудачных попыток;
- версию и CRC32 образа.
Если приложение не выполняет `CONFIRM` за установленное число запусков,
загрузчик возвращается к предыдущему подтверждённому слоту.
## Эталонный сценарий
1. ПМ адресно отправляет `IDENTIFY`.
2. ПМ открывает ненулевой `SessionID` командой `ENTER_BOOT`.
3. ПМ отправляет `BEGIN_IMAGE` и `BEGIN_COMPAT`.
4. Прибор сообщает выбранный неактивный слот.
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`.