嵌入式系统上多个文件的版本控制

Version controll for multiple files on an embedded system

我有一个 esp32-wrover-e(16mb 闪存和 8mb 外部内存) 我正在使用 arduino 框架对其进行编码。

在esp32上,有一个网络服务器,它有多个用于登录、主页等的文件。我想从节点 js 服务器远程升级文件系统上的每个文件。固件升级快完成了。

对于固件,有一个包含固件版本、更新日期和另外两个数组的对象。

像这样:

var firmwareInfos = {
    version : 1.1,
    date : new Date().getTime() / 1000,
    downloads: [],
    queries: []
};

当 esp 请求新版本时,它会向服务器执行 HTTP GET 请求。服务器将此对象发送到 esp,它会决定版本是否更大。如果它在服务器上更大,它将下载新固件(使用单独的 HTTP 请求)。这没问题,而且有效。

现在我也想像这样检查文件及其版本。当 ESP 询问固件版本时,服务器也可以将所有文件版本与固件一起发送。

为此,服务器将有一个包含所有文件路径及其版本的对象,esp32 也应将此对象保存在其文件系统中。这可以工作,但无法扩展。

如果我想将新文件与新固件一起添加到 esp,我必须手动将新文件路径及其版本添加到 esp 和服​​务器的版本对象。

我从服务器端开始使用这个对象:

var versions = {
    firmware : {
        version : 1.1,
        date : new Date().getTime() / 1000,
        downloads: [],
        queries: []
    },
    main: {
        script:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
        style:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
        index:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
    },
    login: {
        script:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
        style:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
        index:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
    },
    admin: {
        script:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
        style:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
        index:{
            version : "1.0",
            date : new Date().getTime() / 1000,
            downloads: [],
            queries: []
        },
    }
};

这将是 esp 所要求的对象。它还不完整,我还没有考虑过这一切,因为正如您所看到的,对象中有多个密钥对,我认为它可以更简单地完成。但同样,这是我能想到的最好的方法,它无法扩展,因为这个对象也必须存在于 esp 上,如果我想扩展 esp 的文件系统,我必须手动向它添加新文件。

另一种解法:

esp 会循环访问它的文件系统,收集其中的所有文件名,将其放入 json 对象并将其发送到服务器,要求所有文件的新版本。服务器会循环处理这个对象,它会检查它的数据库中是否有同名的对象(即 mongo db ),它会创建一个新对象,其中包含它找到的文件的版本。为此,我正在考虑这样的方法:

void fSystem::checkEntireFS(){
    SpiRamJsonDocument doc(15000);
    JsonArray rootArray = doc.to<JsonArray>();
    Serial.println("FS - Start checking entire FS");
    gatherFiles("/", 5, rootArray);
    serializeJsonPretty(doc, Serial);
    Serial.println("FS - End checking entire FS");
}

void fSystem::gatherFiles(const char* dirname, uint8_t levels, JsonArray &rootArray){
    File root = LittleFS.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        String name = file.name();
        JsonObject entryObject = rootArray.createNestedObject();
        entryObject["path"] = dirname;
        entryObject["name"] = name;
        if(file.isDirectory()){
            entryObject["type"] = "dir";
            if(levels){
                gatherFiles(file.path(), levels -1, rootArray);
            }
        } else {
            entryObject["type"] = "file";
            entryObject["size"] = file.size();
            entryObject["date"] = file.getLastWrite();
        }
        vTaskDelay(1);
        file = root.openNextFile();
    }
}

为此使用 ArduinoJson and the built in LittleFS 创建我的文件数组。

这样我就可以使用 getLastWrite()size 来确定 nodejs 服务器上的文件是否更新,而不必依赖版本号。

我也对版本变量感到困惑。如果我想要三位变量,我必须使用 char 数组。如果我使用 long 或 double,我只有像 1.1 这样的两位数变量。

但是对于 char 数组,我必须循环遍历 char 数组并比较每个数字。必须有更好的方法。

所以问题是,最好的方法是什么?你们如何进行多文件版本控制?能不能做的简单点?

感谢您的回答。

编辑*

esp32 位于远离服务器的防火墙后面的网络中。服务器在 public 网络上,但 esp 不在。 esp 无法从外部访问,只能从服务器访问。不幸的是,我在 esp 上的资源非常有限,比如剩下 100kb 的空闲堆。我可以从节点 js 服务器中提取所有文件并刷新所有内容,而不管版本如何,但是有一堆文件,每次请求都需要花费很多时间来下载所有文件。有多个语言文件,每个 15kb,还有每个 70-100kb 的图像。我只想下载需要更新的相关文件。

我假设你这样做的动机是 scalable/sparse 更新。

如果您真的想继续使用自己的版本控制解决方案,可以将 char* 作为文件版本传递给 std::string constructor and then use the == operator for comparison. For detecting changes, I would use the file hash,但日期也适用于 合理同步 系统时钟。如果你的文件只是从服务器上拉出来的(从来没有写在 esp 上),那么你已经知道 esp 需要预先下载的文件并避免 esp 上的文件系统代码。即,如果服务器在请求中获得全局版本 5,而当前全局版本为 8,则发回压缩存档,其中包含版本 58 之间更改的文件.这在延迟和带宽方面是最佳的,您可以在您的服务器上完成所有 繁重的工作。它还将协议本身减少到最低限度。这可以扩展到一种连续的版本控制,您只需将 上次拉取时间 传递给服务器,它就会发回自那时以来更改的文件。

编辑:git 不是 ESP32-Arduino 的选项

然而,所有这些(以及更多)都包含在 git 中。我只是将它安装在两台机器上并通过您的代码自动调用(node.js 上的 child_process.exec 和 c++ 上的 system())。基本上,您需要服务器上的 git push 和机器人上的 git pull。传输快速且可扩展。您甚至可以 self-host git 存储库 remote 在您的服务器机器上,例如如果您想留在本地网络中。

最终解决方案如下。

在管理员的服务器上 UI 管理员可以将任何类型的文件上传到服务器。 (服务器上有一个 esp 文件系统结构的精确副本,具有相同的路径)上传成功后,服务器将从该文件创建一个对象,包含它的路径、名称和上传日期。

服务器跟踪这些上传,在 mongoDB 中保存和更新它们。

当esp请求新文件时,服务器将这个对象交给esp。 esp 循环处理这个对象并根据对象的路径检查它自己的文件系统中的文件。如果找到此路径,则检查它的最后修改日期。如果它较低,则将此文件放在一个单独的数组中并继续循环。如果 esp 在提供的路径上找不到文件,它会认为这是一个新文件,并将此路径也放入单独的数组中。

循环结束后,它会要求服务器下载单独数组中的这些新文件。这就是我如何解决来自服务器的多个文件更新。

文件上传如下所示:

var uploader    = require("express-fileupload");
var moduleHandles   = require("./moduleRoutes.js");

function uploadFile(file,path,res,key){
    file.mv(`${path}/${file.name}`,function(err){
        if(err){
            res.status(400).json({status: "error", message: `${file.name} upload failed!`});
        }else{
            moduleHandles.addNewFileInfo(`${path}/${file.name}`);
            res.status(200).json({status: "success", message: `${file.name} upload success!`});
        }
    });
}

module.exports = {
    initPaths: function(app){
        app.use(uploader({ createParentPath: true }));

        app.post('/fileUpload', function (req, res) {
            let path        = req.query.path;
            let fileKeys    = Object.keys(req.files);
            if( fileKeys.length > 0 ){
                fileKeys.forEach(function(key) {
                    uploadFile(req.files[key],path,res,key);
                });
            }else{
                res.status(400).json({status: "error", message: "Please choose at least one file to be able to upload!"});
            }
        });
    }
}

在模块处理中它看起来像这样:

addNewFileInfo: function(filePath){
        // Removing any unnecessary path for the esp
        let pathKey = filePath.replace("./HsH_Files","");
        let fileInfo = {
            dateSec : parseInt(new Date().getTime() / 1000),
            name    : pathKey.split("/").pop(),
        };
        fileInfos[pathKey] = fileInfo;
        updateFileInfosInDB();
    },
    deleteFileInfoByPath: function(filePath){
        let elemPath = filePath.replace("./HsH_Files","");
        if( fileInfos.hasOwnProperty(elemPath) ){
            delete fileInfos[elemPath];
            asyncFileInfoUpdateInDB();
        }
    },

在 esp 方面还没有完成,但它看起来像这样:

void pSystem::checkNewFiles(){
    HTTPClient http;
    char fileCheckURL[200];
    http.begin( fileCheckURL );
    int httpCode = http.GET();

    if (httpCode > 0) {
        SpiRamJsonDocument infoJsonDoc(FILE_INFO_SIZE);
        DeserializationError error = deserializeJson(infoJsonDoc, http.getStream());
        if( !error ){
            JsonObject infoDoc = infoJsonDoc.as<JsonObject>();
            for (JsonPair infoRef : infoDoc) {
                const char* filePath    = infoRef.key().c_str();
                JsonObject fileInfo     = infoDoc[filePath];

                SpiRamJsonDocument fileDetailsDoc(FILE_DETAILS_SIZE);
                JsonObject fileDetails = fileDetailsDoc.to<JsonObject>();
                hsh_fileSystem.getFileDetailsOnPath(filePath, fileDetails);

                if( fileDetails["result"] ){
                    if( fileDetails["lastModified"].as<long>() < fileInfo["date"].as<long>() ){
                        // push it to downloadable files array.
                    }
                }else if( !fileDetails["result"] ){
                    if( !fileDetails["exists"] ){
                        hsh_fileSystem.createPath(filePath);
                        // push it to downloadable files array.
                    }
                }
            }
        }
    }
    http.end();
}