Загрузка скетча и обновление по сети

Публикация 15.10.2017

Рано или поздно разработчик устройств для своего умного дома сталкивается с проблемой, что таких устройств стало слишком много и обслуживать их стало очень не удобно. Что делать? Постоянно бегать по квартире с кабелем и ноутбуком? Конечно нет! Пришло время задуматься о обновлениях "по воздуху".

Обновление через web-сервер

Описанный выше подход неэффективен и плохо масштабируется. Что бы решить эти проблемы нужно в скетче делать проверку версии раз в N часов или при каждом старте и грузить обновления с единого веб-сервера.

Уменьшается вычислительная нагрузка на само устройство. Процесс обновлений становится плановым и стабильным.

Типовой пример кетча для решения этих задач:

...
t_httpUpdate_return ret = ESPhttpUpdate.update("mysite.ru", 80, "/esp/update.php", "rgb-led_v_1.1.123");
switch(ret) {
    case HTTP_UPDATE_FAILED:
        Serial.println("[update] Update failed."); // обновление выполнить не удалось
        break;
    case HTTP_UPDATE_NO_UPDATES:
        Serial.println("[update] Update no Update."); // обновление не требуется
        break;
    case HTTP_UPDATE_OK:
        Serial.println("[update] Update ok."); // обновление скачивается
        break;
}
...

Тут мы видим, что скетч отправляет название текущей версии к скрипту на сайте http://mysite.ru/esp/update.php Скрипт сам решает отдавать для текущей версии обновление или нет.

Пример серверного скрипта на php:

<?php

header('Content-type: text/plain; charset=utf8', true);

function check_header($name, $value = false) {
    if(!isset($_SERVER[$name])) {
        return false;
    }
    if($value && $_SERVER[$name] != $value) {
        return false;
    }
    return true;
}

function sendFile($path) {
    header($_SERVER["SERVER_PROTOCOL"].' 200 OK', true, 200);
    header('Content-Type: application/octet-stream', true);
    header('Content-Disposition: attachment; filename='.basename($path));
    header('Content-Length: '.filesize($path), true);
    header('x-MD5: '.md5_file($path), true);
    readfile($path);
}

if(!check_header('HTTP_USER_AGENT', 'ESP8266-http-Update')) {
    header($_SERVER["SERVER_PROTOCOL"].' 403 Forbidden', true, 403);
    echo "only for ESP8266 updater!\n";
    exit();
}

if(
    !check_header('HTTP_X_ESP8266_STA_MAC') ||
    !check_header('HTTP_X_ESP8266_AP_MAC') ||
    !check_header('HTTP_X_ESP8266_FREE_SPACE') ||
    !check_header('HTTP_X_ESP8266_SKETCH_SIZE') ||
    !check_header('HTTP_X_ESP8266_CHIP_SIZE') ||
    !check_header('HTTP_X_ESP8266_SDK_VERSION') ||
    !check_header('HTTP_X_ESP8266_VERSION')
) {
    header($_SERVER["SERVER_PROTOCOL"].' 403 Forbidden', true, 403);
    echo "only for ESP8266 updater! (header)\n";
    exit();
}

if('rgb-led_v_1.1.123' != $_SERVER['HTTP_X_ESP8266_VERSION']) ) {
  sendFile('bin/rgb-led_v_1.1.124.bin');
} else {
  header($_SERVER["SERVER_PROTOCOL"].' 304 Not Modified', true, 304);
}
exit();

header($_SERVER["SERVER_PROTOCOL"].' 500 no version for ESP MAC', true, 500);

При увеличенни количества устройств можно доработать код. Сделать список нескольких серверов для скачки обновлений. Сервера будут располагаться на разных доменных именах, на разных физических машинах. При неудачной попытке проверить версию прошивки наш микроконтроллер будет пытаться соединиться с следующим сервером по списку. Таким образом, если у нас один из адресов ляжет, то другие продолжат отдавать данные.

Стоит помнить о некоторых особенностях. Главное - это слеящий код, он выполняется постоянно т.к. ожидает запроса на обновление. Если скетч сильно нагружает процессор, то он может не успеть отслежить пришедший запрос и обновление может быть не загружено с первой попытки.

 

Рассмотрим скетч из примера:

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
 
const char* ssid = "..........";
const char* password = "..........";
 
void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
 
  // Port defaults to 8266
  // ArduinoOTA.setPort(8266);
 
  // Hostname defaults to esp8266-[ChipID]
  // ArduinoOTA.setHostname("myesp8266");
 
  // No authentication by default
  // ArduinoOTA.setPassword("admin");
 
  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
 
  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH)
      type = "sketch";
    else // U_SPIFFS
      type = "filesystem";
 
    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}
 
void loop() {
  ArduinoOTA.handle();
}

Скетч мы загрузили, микроконтроллер перезагрузился и теперь в списке устройств появилась наша плата.

Обновление по локальной сети

Все работает предельно просто. В скетче вашего устройства есть небольшой участок кода для загрузки нового кода. Этот код в цикле проверяет, что к устройству в сети wifi пришел запрос на обновление прошивки. При обнаружении такого запроса - грузит ее и перезагружается для применения обновления. В новой прошивке, в свою очередь, тоже есть "следящий код" для подгрузки обновления.

Принцип работы таких обновлений простой. Нельзя сказать, что он на сто процентов безопасный т.к. всегда есть шанс, что во время очередного обновления не обрубится напряжение или не будет залит кривой скетч. Тогда придется прошивать скетч по старинке, но это форс мажорные случаи. Их мы рассматривать не будем.

Для настройки автоматических обновлений воспользуемся библиотекой OTA.

Найти ее можно в репозитории https://github.com/esp8266/Arduino/tree/master/libraries/ArduinoOTA или в файле (см. приложение).

Файлы для скачивания:
* комментарии публикуются после модерации
16.10.2017 00:12
Ну все, я пошел пилить свой стартап очередной умной розетки) Принимаю предзаказы ахах))
Ну если честно - очень клево. Только надо тщательно продумать безопасность, что бы потом злоумышленники не залили свои скетчи и не начали всех досить или, прости госпади, майнить)
Защита от брутфорса + переход на ssl + хранение на сервере идентификаторов устройств и пр. плюшки решат все проблемы. Так сказать, доработать напильником.
16.10.2017 00:06
Спасибо! Отличная статья!
Я давно мучался вопросом автоматического обновления своих поделок, но не хватало знаний и в интернете ничего похожего найти не смог.