ESP8266 OTA之服务器更新

我之前介绍了两篇关于ESP8266的OTA更新方法:

说实话以上这两种方案除有点好玩其实没有什么实用性,由其在开发产品时上述的两种方案就是一种鸡肋,甚至可以说是不可用的。那现在市面上的各种智能设备是怎么更新固件的呢?

我使用过的绝大多数会进行固件更新的IoT设备都是先在手机上提示有新的固件可以更新,如果一确定以后就会自动下载并进行更新。那我们的ESP8266是否也能做到这样的效果呢?

答案显然是肯定的。本文所讲述的OTA做法其实才是做产品的正道。

首先你要准备一台静态文件服务器,并将某个目录作为更新文件存储。当然,如果你希望以后更方便地管理你发布的更新,那么你的更新文件就需要有良好的命名规则,关于这方面可以到百度上去搜一下找个命名规范或者按自己需要定义一套就OK了。

然后将自更新逻辑写入固件:

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#define USE_SERIAL Serial

ESP8266WiFiMulti WiFiMulti;

void setup() {

    USE_SERIAL.begin(115200);
    // USE_SERIAL.setDebugOutput(true);

    USE_SERIAL.println();
    USE_SERIAL.println();
    USE_SERIAL.println();

    for(uint8_t t = 4; t > 0; t--) {
        USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
        USE_SERIAL.flush();
        delay(1000);
    }

    WiFi.mode(WIFI_STA);
    WiFiMulti.addAP("SSID", "PASSWORD");
}

void loop() {
    // wait for WiFi connection
    if((WiFiMulti.run() == WL_CONNECTED)) {

        t_httpUpdate_return ret = ESPhttpUpdate.update("http://server/file.bin"); // 编译好的固件文件

        switch(ret) {
            case HTTP_UPDATE_FAILED:
                USE_SERIAL.printf("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
                break;

            case HTTP_UPDATE_NO_UPDATES:
                USE_SERIAL.println("HTTP_UPDATE_NO_UPDATES");
                break;

            case HTTP_UPDATE_OK:
                USE_SERIAL.println("HTTP_UPDATE_OK");
                break;
        }
    }
}

看完以上代码是否是会有种想骂人的冲动?我当时找资料看ESP的OTA使用的正确姿势时也有种想骂娘的冲动:太TMD简单了!

原理就只是ESPhttpUpdate.update这个方法,调用一下就可以从服务器下载固件并自动更新了!

这个update方法是有多个重载版本的,上面只是用了其中一个最简单的重载版本,因此只有一个参数指向更新文件。

   // This function is deprecated, use rebootOnUpdate and the next one instead
    t_httpUpdate_return update(const String& url, const String& currentVersion,
                               const String& httpsFingerprint, bool reboot) __attribute__((deprecated));
    t_httpUpdate_return update(const String& url, const String& currentVersion = "");
    t_httpUpdate_return update(const String& url, const String& currentVersion,
                               const String& httpsFingerprint);
    t_httpUpdate_return update(const String& url, const String& currentVersion,
                               const uint8_t httpsFingerprint[20]); // BearSSL

    // This function is deprecated, use one of the overloads below along with rebootOnUpdate
    t_httpUpdate_return update(const String& host, uint16_t port, const String& uri, const String& currentVersion,
                               bool https, const String& httpsFingerprint, bool reboot) __attribute__((deprecated));

    t_httpUpdate_return update(const String& host, uint16_t port, const String& uri = "/",
                               const String& currentVersion = "");
    t_httpUpdate_return update(const String& host, uint16_t port, const String& url,
                               const String& currentVersion, const String& httpsFingerprint);
    t_httpUpdate_return update(const String& host, uint16_t port, const String& url,
                               const String& currentVersion, const uint8_t httpsFingerprint[20]); // BearSSL

以下是ESP8266httpUpdate的其它方法:

void rebootOnUpdate(bool reboot)  // 更新后重启
int getLastError(void); // 获取最后出错的错误编号
String getLastErrorString(void); // 获取出错的信息描述

固件更新服务器

以下是ESPhttpUpdate产生的请求头内容:

[HTTP_USER_AGENT] => ESP8266-http-Update [HTTP_X_ESP8266_STA_MAC] => 18:FE:AA:AA:AA:AA [HTTP_X_ESP8266_AP_MAC] => 1A:FE:AA:AA:AA:AA [HTTP_X_ESP8266_FREE_SPACE] => 671744 [HTTP_X_ESP8266_SKETCH_SIZE] => 373940 [HTTP_X_ESP8266_CHIP_SIZE] => 524288 [HTTP_X_ESP8266_SDK_VERSION] => 1.3.0 [HTTP_X_ESP8266_VERSION] => DOOR-7-g14f53a19

你可以在更新服务器中检测这些请求头是符合法,同时也可以检测当前发出请求的ESP所使用的固件版本是最新的,如果服务端有新的版本可用就重定向到该文件然后就使ESP进行自更新。

以下是用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();
}

$db = array(
    "18:FE:AA:AA:AA:AA" => "DOOR-7-g14f53a19",
    "18:FE:AA:AA:AA:BB" => "TEMP-1.0.0"
);

if(isset($db[$_SERVER['HTTP_X_ESP8266_STA_MAC']])) {
    if($db[$_SERVER['HTTP_X_ESP8266_STA_MAC']] != $_SERVER['HTTP_X_ESP8266_VERSION']) ) {
        sendFile("./bin/".$db[$_SERVER['HTTP_X_ESP8266_STA_MAC']]."bin");
    } else {
        header($_SERVER["SERVER_PROTOCOL"].' 304 Not Modified', true, 304);
    }
    exit();
}

header($_SERVER["SERVER_PROTOCOL"].' 500 no version for ESP MAC', true, 500);
上一篇:《Android 应用案例开发大全(第二版)》——1.5节第一个Android程序——Hello Android


下一篇:“数据结构+算法”视角的Asprova