# Django OTA Update система — Концепция

## Цел

Django уеб приложение, което позволява дистанционно обновяване на фърмуера на
ESP32 платките (retro фърмуер) без физически достъп до устройствата.

---

## Предпоставки

- Django сървърът е на **същата мрежа** като платките (хотелски WiFi)
- Платките са свързани към хотелския WiFi и MQTT брокер
- Платките използват **retro фърмуер** с `AsyncElegantOTA`

---

## Важно: AP режим — кога се създава и кога НЕ

Фърмуерът има **два начина** за активиране на OTA режим с **различно мрежово поведение**:

| Начин | Как се активира | AP | Достъпност от хотелска мрежа |
|-------|----------------|----|------------------------------|
| **MQTT `start_web`** | `mac_subscriber()` → `startServer()` | **БЕЗ AP** | **Да** — устройството остава на хотелски WiFi |
| Физическа карта (update) | `activateUpdate()` → `WiFi.softAP()` → `startServer()` | **С AP** | **НЕ** — достъпно само от самото AP |
| Физическа карта (AP) | `activateAP()` → `WiFi.softAP()` | **С AP** | **НЕ** — достъпно само от самото AP |

### Защо MQTT `start_web` работи за Django:

Когато платката получи `start_web` по MQTT, тя е **вече свързана към хотелския WiFi** и **остава на него**.
`startServer()` ([main.cpp:1610](../src/main.cpp#L1610)) просто стартира уеб сървъра на вече съществуващия
WiFi IP адрес — **`WiFi.softAP()` не се вика**. Django може директно да направи HTTP POST до IP-то на платката.

Физическата карта тригер е предназначен за ръчна употреба на места без хотелски WiFi. Той вика
`WiFi.softAP()` преди `startServer()` и платката е достъпна само ако физически се закачиш към нейното AP.

**Заключение:** Django OTA системата разчита **изключително** на MQTT `start_web` тригера и **не използва AP режима**.

---

## Архитектура

```
┌─────────────────────────────────────────────────────┐
│                  Django сървър                       │
│                                                     │
│  ┌────────────────┐      ┌────────────────────────┐ │
│  │ Firmware store │      │    Device registry     │ │
│  │  firmware.bin  │      │  MAC / стая / IP / ver │ │
│  └───────┬────────┘      └───────────┬────────────┘ │
│          │                           │              │
│  ┌───────▼───────────────────────────▼────────────┐ │
│  │              MQTT клиент (paho)                 │ │
│  │   subscribe: alive, update                      │ │
│  │   publish:   [MAC] → "start_web"  (retained)   │ │
│  └───────────────────────┬─────────────────────────┘ │
│                          │                           │
│  ┌───────────────────────▼─────────────────────────┐ │
│  │         HTTP клиент (requests)                  │ │
│  │   POST http://[device-IP]/update                │ │
│  │   body: firmware.bin                            │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
         ↕ MQTT                    ↕ HTTP POST
┌─────────────────┐         ┌──────────────────────┐
│  ESP32 платки   │         │    MQTT брокер       │
│  (retro fw)     │◄────────│    (Mosquitto)       │
└─────────────────┘         └──────────────────────┘
```

---

## Update flow (стъпка по стъпка)

```
Django                    MQTT брокер              Платка (retro)
  │                           │                         │
  │  [1] Потребителят избира  │                         │
  │      платка и натиска     │                         │
  │      "Update"             │                         │
  │                           │                         │
  │──[2] PUBLISH ────────────>│                         │
  │   topic: 58bf2528fc00     │                         │
  │   payload: "start_web"    │                         │
  │   retained: TRUE          │                         │
  │                           │                         │
  │                           │  [3] Платката се събужда│
  │                           │      (max 60 сек)       │
  │                           │      получава съобщение │
  │                           │<────────────────────────│
  │                           │   subscribe MAC topic   │
  │                           │──────────────────────── >│
  │                           │   "start_web" доставено │
  │                           │                         │ стартира
  │                           │                         │ web server
  │                           │                         │
  │<──[4] PUBLISH ────────────│<────────────────────────│
  │   topic: "alive"          │                         │
  │   payload:                │                         │
  │   "58bf2528fc00/olock/    │                         │
  │    192.168.1.105/H3V_1.0.5│                         │
  │    /Room3614/29/-65/..."  │                         │
  │                           │                         │
  │  [5] Django парсва IP:    │                         │
  │      192.168.1.105        │                         │
  │      потвърждава MAC      │                         │
  │                           │                         │
  │──[6] HTTP POST ───────────│────────────────────────>│
  │   http://192.168.1.105    │                         │
  │         /update           │                         │
  │   body: firmware.bin      │                         │
  │                           │                 flash + │
  │                           │                 restart │
  │                           │                         │
  │──[7] PUBLISH ────────────>│                         │
  │   topic: 58bf2528fc00     │                         │
  │   payload: ""             │                         │
  │   retained: TRUE          │                         │
  │   (изчиства retained cmd) │                         │
```

---

## Компоненти

### 1. Firmware store
- Django `FileField` — съхранява качените `.bin` файлове
- Обслужва файловете по HTTP: `http://django-server/media/firmware/firmware.bin`
- Съхранява версия и дата на качване

### 2. Device registry
Попълва се автоматично от `alive` MQTT съобщенията на платките.

**Формат на `alive` съобщение:**
```
[MAC]/[type]/[IP]/[version]/[name][room]/[battery%]/[RSSI]/[datetime]

Пример:
58bf2528fc00/olock/192.168.1.105/H3V_1.0.5/Room3614/29/-65/25-06-2026 11:36:13
```

Полета в базата данни:
| Поле | Пример | Описание |
|------|--------|----------|
| mac | 58bf2528fc00 | уникален идентификатор |
| ip | 192.168.1.105 | последно известен IP |
| firmware_version | H3V_1.0.5 | текуща версия |
| room | 3614 | стая |
| battery | 29 | % заряд |
| rssi | -65 | WiFi сигнал dBm |
| last_seen | 25-06-2026 11:36 | последна активност |

### 3. MQTT клиент
Фонов thread стартиран при `AppConfig.ready()`:
- **Subscribe:** `alive`
- **Publish:** `[MAC]` с payload `start_web` (retained)
- Актуализира Device registry при всяко `alive` съобщение

> **Забележка:** `update` топикът се използва само при физическа карта тригер
> (`activateUpdate()` сетва `awake_update = true`). При MQTT `start_web` тригер
> устройството публикува IP-то си на `alive` — преди да обработи `start_web`.

### 4. Update процес
Django изпълнява следната последователност:
1. Публикува `start_web` (retained) към MAC на устройството
2. Слуша `alive` топика с timeout (макс. 2 минути)
3. При получаване на `alive` съобщение с познат MAC → взима IP-то
4. `requests.post(f"http://{ip}/update", files=...)` — web сървърът вече е стартиран
5. Публикува празно retained съобщение за изчистване

---

## Технически детайли

### MQTT команди към платките

| Topic | Payload | Ефект |
|-------|---------|-------|
| `[MAC]` | `start_web` (retained) | Стартира OTA web сървъра |
| `[MAC]` | `ping` | Платката отговаря с alive |
| `[MAC]` | `restart` | Рестартира платката |
| `[MAC]` | `` (празно, retained) | Изчиства retained команда |
| `all` | `start_web` | Всички платки едновременно |

### HTTP POST към AsyncElegantOTA
```
POST http://[device-IP]/update
Content-Type: multipart/form-data

[firmware.bin binary data]
```
> Точният field name се верифицира с Browser DevTools (Network tab)
> при ръчно качване към `/update` endpoint.

### Timing
| Събитие | Време |
|---------|-------|
| Платката се събужда (timer) | макс. 60 сек |
| Прозорец за OTA след `start_web` | 5 мин (UPDATE_TIME) |
| Django timeout за чакане на IP | 2 мин (конфигурируемо) |

---

## Django технологии

| Нужда | Библиотека |
|-------|-----------|
| MQTT publish/subscribe | `paho-mqtt` |
| HTTP POST до платката | `requests` |
| Файлово съхранение | Django `FileField` + `MEDIA_ROOT` |
| Фонов MQTT thread | `AppConfig.ready()` + `threading` |

---

## Отворени въпроси

1. **POST формат** — точен field name за AsyncElegantOTA `/update` endpoint
   (верифицира се с DevTools при ръчен upload)

2. **Масов update** — поредно или паралелно за множество платки?

3. **Статус на update** — как Django разбира дали update-ът е успешен?
   (платката след рестарт ще публикува нова `alive` с новата версия)

4. **Автентикация** — AsyncElegantOTA поддържа username/password.
   Трябва ли Django да ги изпраща в POST заявката?

5. **Fallback** — ако платката не отговори в рамките на 2 мин,
   какво прави Django? (retry, alert, skip)
