Сеть и обновление прошивки

Событийное подключение Wi‑Fi и полный цикл безопасного OTA-обновления.

11. WiFi - STA-подключение

Событийная state-machine поверх (режим STA). Включается PIPCORE_ENABLE_WIFI. Радио работает, пока запрошено wifiRequest(true); при wifiRequest(false) железо полностью деинициализируется.

Важно: таймауты подключения и повторные попытки продвигаются только внутри wifiService() - вызывайте её регулярно (например, каждый проход главного цикла).

Конфигурация

struct WifiConfig
{
    char ssid[kWifiSsidCap] = {};          // kWifiSsidCap = 33
    char password[kWifiPasswordCap] = {};  // kWifiPasswordCap = 65

    bool disableSleep = false;             // true → WIFI_PS_NONE (максимальная
                                           // пропускная способность)

    bool fullScan = false;                 // true → сканировать все каналы
                                           // (медленнее, надёжнее)

    bool autoReconnect = true;             // автоматический реконнект с
                                           // нарастающей паузой

    uint32_t connectTimeoutMs = 15'000;    // таймаут одной попытки
    uint32_t retryDelayMs = 2'500;         // базовая пауза между попытками

    uint32_t staticIp = 0;                 // статический IPv4, формат - см. ниже
    
    uint32_t gateway = 0;                  // если staticIp, gateway и subnet
                                           // все != 0 - DHCP отключается
    uint32_t subnet = 0;
    uint32_t dns1 = 0;                     // необязательные DNS
    uint32_t dns2 = 0;

    void setCredentials(const char *ssidIn, const char *passwordIn);  // безопасное копирование
};

enum class WifiState : uint8_t
{
    Off = 0, Connecting = 1, Connected = 2, Failed = 3, Unsupported = 4
};

Функции

Функция Описание
void wifiConfigure(const WifiConfig &cfg) Задать креды и параметры. Вызывайте до включения радио
void wifiRequest(bool enabled) Включить или выключить радио
void wifiService() Прокачка state-machine. Вызывайте в цикле
WifiState wifiState() Текущее состояние
bool wifiConnected() state == Connected
uint32_t wifiLocalIpV4() Локальный IPv4 (0 - адреса нет)
WifiState Значение
Off Радио выключено
Connecting Идёт подключение или пауза перед повторной попыткой
Connected Подключено, IP получен
Failed Ошибка (в том числе пустой SSID); повторная попытка - по retryDelayMs
Unsupported WiFi-бэкенд недоступен

Пример

net::WifiConfig cfg = {};
cfg.setCredentials("MyWiFi", "password");
cfg.disableSleep = true;
net::wifiConfigure(cfg);
net::wifiRequest(true);

const uint32_t start = plat->nowMs();
while (!net::wifiConnected() && plat->nowMs() - start < 20'000)
{
    net::wifiService();
    plat->delayMs(10);
}

if (net::wifiConnected())
{
    const uint32_t ip = net::wifiLocalIpV4();
    log::info("IP: %u.%u.%u.%u",
              unsigned(ip >> 24), unsigned((ip >> 16) & 0xFF),
              unsigned((ip >> 8) & 0xFF), unsigned(ip & 0xFF));
}

Формат IP-адресов

Адреса хранятся как uint32_t в порядке «старший октет - в старших битах»: 192.168.1.10 → 0xC0A8010A; ip >> 24 - первый октет. Это относится и к staticIp, gateway, subnet, dns1, dns2.

Повторные попытки

Если попытка подключения не удалась (таймаут или обрыв), состояние сменяется на Failed / Connecting, и через retryDelayMs начинается новая попытка. При автореконнекте пауза удваивается с каждой неудачей до 16× от базовой - сеть не «заваливается» запросами при лежащем роутере. Ручной реконнект - снова wifiRequest(true).

12. OTA - обновления прошивки

Обновление по манифесту с криптографической проверкой. Включается PIPCORE_ENABLE_OTA (требует WiFi). Сервис сам включает радио на время работы, поэтому WiFi-креды достаточно задать через net::wifiConfigure() - вызывать wifiRequest(true) не нужно.

Поток обновления

GET {URL}/index.json
   → выбор канала (stable / beta) и более нового build
GET {URL}/<релиз>/manifest.json
   → проверка Ed25519-подписи
   → состояние UpdateAvailable
requestInstall()
   → потоковая загрузка с подсчётом SHA-256
   → запись во второй OTA-слот
   → смена загрузочного раздела
   → перезагрузка

Anti-replay: build не может быть меньше сохранённого для канала (хранится в NVS).

Типы

struct Options
{
    uint16_t currentVerMajor = 0, currentVerMinor = 0, currentVerPatch = 0;
    uint64_t currentBuild = 0;               // монотонный счётчик сборки
    const char *ed25519PubkeyHex = nullptr;  // открытый ключ, 64 hex-символа
};

enum class Channel   : uint8_t { Stable = 0, Beta = 1 };
enum class CheckMode : uint8_t { NewerOnly = 0, AllowDowngrade = 1 };

enum class State : uint8_t
{
    Idle = 0, WifiStarting = 1, FetchingManifest = 2, UpdateAvailable = 3,
    Downloading = 4, Installing = 5, Success = 6, Error = 7, UpToDate = 8
};

struct Manifest
{
    char     title[64];
    uint16_t verMajor, verMinor, verPatch;
    char     version[32];
    uint64_t build;
    uint32_t size;               // размер прошивки, байт
    char     url[512];
    char     desc[256];
    uint8_t  sha256[32];         // контрольная сумма образа
    uint8_t  sigEd25519[64];     // подпись манифеста
    bool     hasSig;
};

struct Status
{
    State state;  Error error;
    int httpCode;                // HTTP-код последнего запроса
    int platformCode;            // код для диагностики
    uint32_t downloaded, total;  // прогресс загрузки, байт
    uint32_t lastChangeMs;       // время последней смены состояния
    int8_t versionCmp, buildCmp; // сравнение с текущей прошивкой
    bool pendingVerify;          // прошивка ждёт подтверждения
    Channel channel;
    Manifest manifest;
};

using StatusCallback = void (*)(const Status &st, void *user);

Коды ошибок (ota::Error)

Код Значение Причина
None (0) - Ошибки нет
WifiNotEnabled (1) WiFi недоступен OTA-сервису не удалось получить WiFi-бэкенд
WifiNotConnected (2) Нет подключения WiFi перешёл в Failed (проверьте креды через net::wifiConfigure)
HttpBeginFailed (3) Не удалось начать HTTP-запрос Проверьте URL
HttpStatusNotOk (4) Сервер вернул не 200 См. Status::httpCode
ManifestTooLarge (5) Манифест слишком большой -
ManifestParseFailed (6) Манифест не разобран Неверный JSON/поля
ManifestReplay (7) Build меньше сохранённого Защита от отката на старую версию
SignatureMissing (8) Нет подписи -
SignatureInvalid (9) Подпись неверна Не тот ключ или манифест изменён
FlashLayoutInvalid (10) Неподходящая разметка флеш Проверьте таблицу разделов: нужен OTA-слот под образ
RollbackUnavailable (11) Откат невозможен Зарезервирован
UpdateBeginFailed (12) Не удалось начать запись -
UpdateWriteFailed (13) Ошибка записи -
HashPipelineFailed (14) Ошибка подсчёта SHA-256 -
DownloadTruncated (15) Загрузка оборвана -
PayloadSizeMismatch (16) Размер не совпал с манифестом -
HashMismatch (17) Контрольная сумма не совпала -
UpdateEndFailed (18) Не удалось завершить обновление -
UrlTooLong (19) URL длиннее буфера -

Функции

Функция Описание
void configure(const Options &opt, StatusCallback cb = nullptr, void *user = nullptr) Инициализация. Базовый URL берётся из PIPCORE_OTA_PROJECT_URL. cb вызывается на каждое изменение статуса
void requestCheck() / void requestCheck(CheckMode mode) Проверить обновления. NewerOnly - только более новые build; AllowDowngrade - разрешить и более старые
void requestInstall() Установить найденное обновление (после UpdateAvailable)
void requestStableList() Запросить список стабильных версий сервера
bool stableListReady() Список получен
uint8_t stableListCount() Число версий в списке
const char *stableListVersion(uint8_t idx) Версия по индексу
void requestInstallStableVersion(const char *version) Установить конкретную версию (откат)
void cancel() Прервать текущую операцию
void service() Прокачка state-machine. Вызывайте в цикле. Блокируется не дольше PIPCORE_OTA_HTTP_TIMEOUT_MS за вызов
const Status &status() Полный статус
void markAppValid() Подтвердить работоспособность прошивки после OTA-перезагрузки

Пример

ota::Options opt = {};
opt.currentVerMajor = 2;  opt.currentVerMinor = 0;  opt.currentVerPatch = 0;
opt.currentBuild = 2000000;
opt.ed25519PubkeyHex = PIPCORE_PUBKEY;    // ваш публичный ключ, 64 hex-символа

ota::configure(opt, onStatus, nullptr);
ota::requestCheck();

// в главном цикле:
ota::service();

const ota::Status &st = ota::status();
if (st.state == ota::State::UpdateAvailable)
    ota::requestInstall();                // например, после подтверждения пользователя
else if (st.state == ota::State::Error)
    log::error("ota: error %d (http %d)", int(st.error), st.httpCode);

Подтверждение после обновления

Новая прошивка стартует в состоянии pending-verify. Если приложение не вызовет markAppValid(), загрузчик IDF откатится на предыдущую версию. Вызывайте markAppValid(), когда убедились, что всё работает (сеть поднялась, железо ответило):

extern "C" void app_main()
{
    // ... инициализация железа ...
    if (initOk)
        ota::markAppValid();
}

Безопасность

  • TLS: сертификат сервера проверяется системным bundle ESP-IDF.
  • Подпись: манифест подписывается Ed25519 по каноническому представлению полей (префикс pipcore-ota-manifest, title, version, build, size, sha256, url, desc). Меняйте эти поля только инструментом публикации - иначе подпись станет недействительной.
  • Целостность: образ проверяется по SHA-256 из подписанного манифеста.