# План реализации Phase 1: Серверная часть OTA обновлений прошивки

## Цель
Создать серверный каталог прошивок, API для управления ими, и механизм push-OTA на устройства.

## Изменения

### 1. Конфигурация (`server/SHServ/config.php`)
- Добавить поле `firmwares_dir` — путь к директории с ZIP-архивами прошивок.
- Значение по умолчанию: `__DIR__ . '/../../firmwares'`.
- Директория должна автоматически создаваться при старте (в `FirmwareCatalog::__construct`).

### 2. Каталог прошивок (`server/SHServ/Tools/FirmwareCatalog.php`)
Новый класс, отвечающий за сканирование, кэширование и поиск прошивок.

**Формат манифеста (`manifest.json` внутри ZIP):**
```json
{
  "id": "relay-1.3.0-esp8266-x8",
  "device_type": "relay",
  "platform": "esp8266",
  "channels": 8,
  "version": "1.3.0",
  "core_version": "1.5.0",
  "bin_filename": "relay.ino.bin",
  "description": "...",
  "changelog": "..."
}
```
- `id`, `device_type`, `version`, `bin_filename` — обязательные.
- `platform`, `channels` — опциональные; если указаны, участвуют в matching.

**API класса:**
- `scan(): array` — сканирует `firmwares_dir/*.zip`, распаковывает каждый ZIP (временно), читает `manifest.json`, валидирует, сохраняет в статический кэш. Возвращает кэш.
- `getAll(): array` — возвращает все записи каталога (массив манифестов).
- `getById(string $id): ?array` — одна запись каталога по ID.
- `getBinPath(string $id): ?string` — распаковывает `.bin` из ZIP во временную директорию, возвращает абсолютный путь к файлу. Cleanup — по желанию вызывающего.
- `findCompatible(array $deviceAbout): array` — фильтрует кэш по `device_type`, `platform`, `channels`, и возвращает только те прошивки, чья `version` > текущей версии устройства. Сравнение версий — компонентное `X.Y.Z`.
- `clearCache(): void` — сброс кэша.

**Распаковка ZIP:**
- Первичный метод: `ZipArchive` (PHP встроенный).
- Fallback: `exec('unzip -o ...')` во временную директорию (`sys_get_temp_dir() . '/shserv_firmwares/...'`).
- После чтения манифеста — временная директория удаляется.

### 3. DeviceAPI (`server/SHServ/Tools/DeviceAPI/Base.php`)
Добавить метод:
```php
public function updateFirmware(string $binPath): array
```
- Отправляет `POST /update` на устройство через multipart/form-data (`CURLFile`).
- Таймаут 60 секунд (OTA длительная операция).
- Отправляет Bearer-токен, если есть.
- Возвращает массив `['http_code' => int, 'raw' => string, 'error' => string|null]`.
- Не использует retry-логику (OTA не идемпотентна).

### 4. Routes (`server/SHServ/Routes/FirmwareRESTAPI_v1.php`)
Новый trait с endpoint'ами:

**URI routes:**
- `GET /api/v1/firmwares` → `FirmwareRESTAPIController@firmwares_list`
- `GET /api/v1/firmwares/id/$firmware_id` → `FirmwareRESTAPIController@firmware_detail`
- `GET /api/v1/firmwares/id/$firmware_id/download` → `FirmwareRESTAPIController@firmware_download`
- `GET /api/v1/devices/id/$device_id/firmware-compatibility` → `FirmwareRESTAPIController@device_firmware_compatibility`

**POST routes:**
- `POST /api/v1/firmwares/refresh` (без body) → `FirmwareRESTAPIController@firmware_refresh`
- `POST /api/v1/devices/update-firmware` (`device_id`, `firmware_id`) → `FirmwareRESTAPIController@device_update_firmware`

### 5. Controller (`server/SHServ/Controllers/FirmwareRESTAPIController.php`)
Новый контроллер, расширяет `\SHServ\Middleware\Controller`.

**Методы:**
- `firmwares_list()` — возвращает массив всех манифестов.
- `firmware_detail($firmware_id)` — один манифест по ID.
- `firmware_download($firmware_id)` — распаковывает `.bin`, отдаёт `Content-Type: application/octet-stream` через `readfile()`, затем cleanup и `exit`.
- `firmware_refresh()` — вызывает `$catalog->scan()`, возвращает success.
- `device_firmware_compatibility($device_id)` — берёт устройство, запрашивает `/about`, передаёт в `findCompatible()`.
- `device_update_firmware($device_id, $firmware_id)` — валидация, проверка совместимости, вызов `updateFirmware()`, cleanup temp bin, возврат результата.

**Обработка ошибок:**
- `firmware_not_found` — прошивка не найдена в каталоге.
- `firmware_not_compatible` — прошивка не подходит для устройства.
- `ota_failed` — устройство вернуло HTTP != 200 или cURL ошибка.

### 6. Интеграция маршрутов (`server/SHServ/Routes.php`)
- Добавить `use \SHServ\Routes\FirmwareRESTAPI_v1;`.
- В `routes_init()` вызвать `$this->firmware_restapi_uri_routes()`, `$this->firmware_restapi_post_routes()`.

### 7. Локализация (`server/SHServ/text-msgs.php`)
Добавить алиасы:
- `firmware_not_found` → "Прошивка не найдена"
- `firmware_not_compatible` → "Прошивка не совместима с устройством"
- `ota_failed` → "Не удалось обновить прошивку на устройстве"

### 8. Autoload
- После создания новых PHP-классов запустить `composer dump-autoload` в `server/`.

### 9. База данных
- **Изменений в схему БД не требуется.** Каталог полностью in-memory, на основе файловой системы.

## Последовательность реализации
1. `config.php` — добавить `firmwares_dir`.
2. `FirmwareCatalog.php` — создать класс.
3. `Base.php` — добавить `updateFirmware()`.
4. `FirmwareRESTAPI_v1.php` — trait с маршрутами.
5. `FirmwareRESTAPIController.php` — контроллер.
6. `Routes.php` — подключить trait.
7. `text-msgs.php` — добавить сообщения об ошибках.
8. `composer dump-autoload`.
9. Тестовый ZIP-архив (вручную) для проверки.

## Риски / Открытые вопросы
- **Таймаут OTA:** 60 секунд достаточно для ESP8266/ESP32 по Wi-Fi? Если нет, увеличим до 120.
- **Cleanup temp файлов:** при ошибках в `device_update_firmware` и `firmware_download` нужно не забыть удалить распакованный `.bin`. Будем использовать `finally` или inline cleanup.
- **Права доступа:** endpoint'ы пока без middleware-авторизации (как и остальные API), позже подключим.
