From 715914bdd0e9f3cd312ed61e2ab7f5a1fd33b9d8 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 7 Feb 2024 22:55:39 +0100 Subject: [PATCH] Add internal support for persistent JSON settings using single file --- CHANGELOG.md | 1 + tasmota/tasmota_support/settings.ino | 17 +- .../xdrv_122_file_json_settings_demo.ino | 230 ++++++++++++++++++ .../xdrv_122_file_settings_demo.ino | 4 +- .../xdrv_50_filesystem.ino | 219 +++++++++++++++++ 5 files changed, 462 insertions(+), 9 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_122_file_json_settings_demo.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ada7fc65..348be9510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ## [13.3.0.5] ### Added +- Internal support for persistent JSON settings using single file ### Breaking Changed - ESP32 LVGL library from v8.3.11 to v9.0.0, some small breaking changes in C, none in HASPmota (#20659) diff --git a/tasmota/tasmota_support/settings.ino b/tasmota/tasmota_support/settings.ino index cd31f2ee3..1ee96f41b 100644 --- a/tasmota/tasmota_support/settings.ino +++ b/tasmota/tasmota_support/settings.ino @@ -420,7 +420,7 @@ bool SettingsBufferAlloc(uint32_t upload_size) { } else { char filename[14]; for (uint32_t i = 0; i < 129; i++) { - snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), i); + snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), i); // /.drvset012 uint32_t fsize = TfsFileSize(filename); if (fsize) { if (settings_size == sizeof(TSettings)) { @@ -466,7 +466,7 @@ uint32_t SettingsConfigBackup(void) { filebuf_ptr += sizeof(TSettings); char filename[14]; for (uint32_t i = 0; i < 129; i++) { - snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), i); // /.drvset012 + snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), i); // /.drvset012 uint32_t fsize = TfsFileSize(filename); if (fsize) { // Add tar header with file size @@ -474,7 +474,7 @@ uint32_t SettingsConfigBackup(void) { filebuf_ptr[14] = fsize; filebuf_ptr[15] = fsize >> 8; filebuf_ptr += 16; - if (XdrvCallDriver(i, FUNC_RESTORE_SETTINGS)) { // Enabled driver + if (i && (XdrvCallDriver(i, FUNC_RESTORE_SETTINGS))) { // Enabled driver // Use most relevant config data which might not have been saved to file // AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Backup driver %d"), i); uint32_t data_size = fsize; // Fix possible buffer overflow @@ -565,10 +565,13 @@ bool SettingsConfigRestore(void) { uint32_t driver = atoi((const char*)filebuf_ptr +8); // /.drvset012 = 12 uint32_t fsize = filebuf_ptr[15] << 8 | filebuf_ptr[14]; // Tar header settings size filebuf_ptr += 16; // Start of file settings - uint32_t buffer_crc32 = filebuf_ptr[3] << 24 | filebuf_ptr[2] << 16 | filebuf_ptr[1] << 8 | filebuf_ptr[0]; - bool valid_buffer = (GetCfgCrc32(filebuf_ptr +4, fsize -4) == buffer_crc32); + bool valid_buffer = true; + if (driver) { + uint32_t buffer_crc32 = filebuf_ptr[3] << 24 | filebuf_ptr[2] << 16 | filebuf_ptr[1] << 8 | filebuf_ptr[0]; + valid_buffer = (GetCfgCrc32(filebuf_ptr +4, fsize -4) == buffer_crc32); + } if (valid_buffer) { - if (XdrvCallDriver(driver, FUNC_RESTORE_SETTINGS)) { + if (driver && (XdrvCallDriver(driver, FUNC_RESTORE_SETTINGS))) { // Restore live config data which will be saved to file before restart // AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Restore driver %d"), driver); filebuf_ptr[1]++; // Force invalid crc32 to enable auto upgrade after restart @@ -1120,8 +1123,8 @@ void SettingsDefaultSet2(void) { flag5.mqtt_switches |= MQTT_SWITCHES; flag5.mqtt_persistent |= ~MQTT_CLEAN_SESSION; flag6.mqtt_disable_sserialrec |= MQTT_DISABLE_SSERIALRECEIVED; -// flag.mqtt_serial |= 0; flag6.mqtt_disable_modbus |= MQTT_DISABLE_MODBUSRECEIVED; +// flag.mqtt_serial |= 0; flag.device_index_enable |= MQTT_POWER_FORMAT; flag3.time_append_timezone |= MQTT_APPEND_TIMEZONE; flag3.button_switch_force_local |= MQTT_BUTTON_SWITCH_FORCE_LOCAL; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_122_file_json_settings_demo.ino b/tasmota/tasmota_xdrv_driver/xdrv_122_file_json_settings_demo.ino new file mode 100644 index 000000000..1b61874f0 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_122_file_json_settings_demo.ino @@ -0,0 +1,230 @@ +/* + xdrv_122_file_json_settings_demo.ino - Demo for Tasmota + + Copyright (C) 2024 Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Enable this define to use this demo +//#define USE_DRV_FILE_JSON_DEMO + +#ifdef USE_DRV_FILE_JSON_DEMO +/*********************************************************************************************\ + * JSON settings load and save demo using Tasmota file system + * + * To test this file: + * - Have hardware with at least 2M flash + * - Enable a board with at least 256k filesystem in platform_override.ini +\*********************************************************************************************/ +#warning **** USE_DRV_FILE_JSON_DEMO is enabled **** + +#define XDRV_122 122 +#define XDRV_KEY "drvset122" + +#define DRV_DEMO_MAX_DRV_TEXT 16 + +const uint16_t DRV_DEMO_VERSION = 0x0105; // Latest driver version (See settings deltas below) + +// Global structure containing driver saved variables +struct { + uint32_t crc32; // To detect file changes + uint16_t version; // To detect driver function changes + char drv_text[DRV_DEMO_MAX_DRV_TEXT][10]; +} DrvDemoSettings; + +// Global structure containing driver non-saved variables +struct { + uint32_t any_value; +} DrvDemoGlobal; + +/*********************************************************************************************\ + * Driver Settings load and save +\*********************************************************************************************/ + +bool DrvDemoLoadData(void) { + char key[20]; + snprintf_P(key, sizeof(key), PSTR(XDRV_KEY)); + String json = UfsJsonSettingsRead(key); + if (json.length() == 0) { return false; } + + JsonParser parser((char*)json.c_str()); + JsonParserObject root = parser.getRootObject(); + if (!root) { return false; } + + JsonParserToken val = root[PSTR("Crc")]; + if (val) { + DrvDemoSettings.crc32 = val.getUInt(); + } + val = root[PSTR("Version")]; + if (val) { + DrvDemoSettings.version = val.getInt(); + } + JsonParserArray arr = root[PSTR("Text")]; + if (arr) { + for (uint32_t i = 0; i < DRV_DEMO_MAX_DRV_TEXT; i++) { + if (arr[i]) { + snprintf(DrvDemoSettings.drv_text[i], 10, arr[i].getStr()); + } + } + } + return true; +} + +bool DrvDemoSaveData(void) { + Response_P(PSTR("{\"" XDRV_KEY "\":{\"Crc\":%u,\"Version\":%u,\"Text\":["), DrvDemoSettings.crc32, DrvDemoSettings.version); + for (uint32_t i = 0; i < DRV_DEMO_MAX_DRV_TEXT; i++) { + ResponseAppend_P(PSTR("%s\"%s\""), (i)?",":"", DrvDemoSettings.drv_text[i]); + } + ResponseAppend_P(PSTR("]}}")); + return UfsJsonSettingsWrite(ResponseData()); +} + +void DrvDemoDeleteData(void) { + char key[20]; + snprintf_P(key, sizeof(key), PSTR(XDRV_KEY)); + UfsJsonSettingsDelete(key); // Use defaults +} + +/*********************************************************************************************/ + +void DrvDemoSettingsLoad(bool erase) { + // Called from FUNC_PRE_INIT (erase = 0) once at restart + // Called from FUNC_RESET_SETTINGS (erase = 1) after command reset 4, 5, or 6 + + // *** Start init default values in case key is not found *** + AddLog(LOG_LEVEL_INFO, PSTR("DRV: " D_USE_DEFAULTS)); + + memset(&DrvDemoSettings, 0x00, sizeof(DrvDemoSettings)); + DrvDemoSettings.version = DRV_DEMO_VERSION; + // Init any other parameter in struct DrvDemoSettings + snprintf_P(DrvDemoSettings.drv_text[0], sizeof(DrvDemoSettings.drv_text[0]), PSTR("Azalea")); + + // *** End Init default values *** + +#ifndef USE_UFILESYS + AddLog(LOG_LEVEL_INFO, PSTR("CFG: Demo use defaults as file system not enabled")); +#else + // Try to load key + if (erase) { + DrvDemoDeleteData(); + } + else if (DrvDemoLoadData()) { + if (DrvDemoSettings.version != DRV_DEMO_VERSION) { // Fix version dependent changes + + // *** Start fix possible setting deltas *** + if (DrvDemoSettings.version < 0x0103) { + AddLog(LOG_LEVEL_INFO, PSTR("CFG: Update oldest version restore")); + + snprintf_P(DrvDemoSettings.drv_text[1], sizeof(DrvDemoSettings.drv_text[1]), PSTR("Begonia")); + } + if (DrvDemoSettings.version < 0x0104) { + AddLog(LOG_LEVEL_INFO, PSTR("CFG: Update old version restore")); + + } + + // *** End setting deltas *** + + // Set current version and save settings + DrvDemoSettings.version = DRV_DEMO_VERSION; + DrvDemoSettingsSave(); + } + AddLog(LOG_LEVEL_INFO, PSTR("CFG: Demo loaded from file")); + } + else { + // File system not ready: No flash space reserved for file system + AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Demo use defaults as file system not ready or key not found")); + } +#endif // USE_UFILESYS +} + +void DrvDemoSettingsSave(void) { + // Called from FUNC_SAVE_SETTINGS every SaveData second and at restart +#ifdef USE_UFILESYS + uint32_t crc32 = GetCfgCrc32((uint8_t*)&DrvDemoSettings +4, sizeof(DrvDemoSettings) -4); // Skip crc32 + if (crc32 != DrvDemoSettings.crc32) { + // Try to save file /.drvset122 + DrvDemoSettings.crc32 = crc32; + + if (DrvDemoSaveData()) { + AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Demo saved to file")); + } else { + // File system not ready: No flash space reserved for file system + AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: ERROR Demo file system not ready or unable to save file")); + } + } +#endif // USE_UFILESYS +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +// Demo command line commands +const char kDrvDemoCommands[] PROGMEM = "Drv|" // Prefix + "Text"; + +void (* const DrvDemoCommand[])(void) PROGMEM = { + &CmndDrvText }; + +void CmndDrvText(void) { + if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= DRV_DEMO_MAX_DRV_TEXT)) { + if (!XdrvMailbox.usridx) { + // Command DrvText + for (uint32_t i = 0; i < DRV_DEMO_MAX_DRV_TEXT; i++) { + AddLog(LOG_LEVEL_DEBUG, PSTR("DRV: DrvText%02d %s"), i, DrvDemoSettings.drv_text[i]); + } + ResponseCmndDone(); + } else { + // Command DrvText + uint32_t index = XdrvMailbox.index -1; + if (XdrvMailbox.data_len > 0) { + snprintf_P(DrvDemoSettings.drv_text[index], sizeof(DrvDemoSettings.drv_text[index]), XdrvMailbox.data); + } + ResponseCmndIdxChar(DrvDemoSettings.drv_text[index]); + } + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv122(uint32_t function) { + bool result = false; + + switch (function) { + case FUNC_RESET_SETTINGS: + DrvDemoSettingsLoad(1); + break; + case FUNC_SAVE_SETTINGS: + DrvDemoSettingsSave(); + break; + case FUNC_COMMAND: + result = DecodeCommand(kDrvDemoCommands, DrvDemoCommand); + break; + case FUNC_PRE_INIT: + DrvDemoSettingsLoad(0); + break; + case FUNC_SAVE_BEFORE_RESTART: + // !!! DO NOT USE AS IT'S FUNCTION IS BETTER HANDLED BY FUNC_SAVE_SETTINGS !!! + break; + case FUNC_ACTIVE: + result = true; + break; + } + return result; +} + +#endif // USE_DRV_FILE_DEMO \ No newline at end of file diff --git a/tasmota/tasmota_xdrv_driver/xdrv_122_file_settings_demo.ino b/tasmota/tasmota_xdrv_driver/xdrv_122_file_settings_demo.ino index 7680df760..c00221b60 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_122_file_settings_demo.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_122_file_settings_demo.ino @@ -109,11 +109,11 @@ void DrvDemoSettingsLoad(bool erase) { if (DrvDemoSettings.version != DRV_DEMO_VERSION) { // Fix version dependent changes // *** Start fix possible setting deltas *** - if (Settings->version < 0x01010100) { + if (DrvDemoSettings.version < 0x01010100) { AddLog(LOG_LEVEL_INFO, PSTR("CFG: Update oldest version restore")); } - if (Settings->version < 0x01010101) { + if (DrvDemoSettings.version < 0x01010101) { AddLog(LOG_LEVEL_INFO, PSTR("CFG: Update old version restore")); } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino index df3dd7260..0e5576669 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino @@ -538,6 +538,225 @@ bool UfsExecuteCommandFile(const char *fname) { return false; } +/*********************************************************************************************\ + * File JSON settings support using file /.drvset000 + * + * {"UserSet1":{"Param1":123,"Param2":"Text1"},"UserSet2":{"Param1":123,"Param2":"Text2"},"UserSet3":{"Param1":123,"Param2":"Text3"}} +\*********************************************************************************************/ + +bool _UfsJsonSettingsUpdate(const char* data) { + // Delete: Input UserSet2 + // Append: Input {"UserSet2":{"Param1":123,"Param2":"Text2"}} + + char filename[14]; + snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), 0); // /.drvset000 + if (!TfsFileExists(filename)) { return false; } // Error - File not found + + char bfname[14]; + strcpy_P(bfname, PSTR("/settmp")); + File ofile = ffsp->open(bfname, "w"); + if (!ofile) { return false; } // Error - unable to open temporary file + File ifile = ffsp->open(filename, "r"); + if (!ifile) { + ofile.close(); + ffsp->remove(bfname); + return false; // Error - unable to open settings file + } + + bool append = false; + char* key = (char*)data; + char key_pos[32]; // Max key length + char *p = strchr(data, '"'); + if (p) { + append = true; + char *q = strchr(++p, '"'); + if (!q) { return false; } // Error - No valid key provided in data to append + uint32_t len = (uint32_t)q - (uint32_t)p; + memcpy(key_pos, p, len); + key_pos[len] = '\0'; // key = UserSet2 + key = key_pos; + } + + char buffer[32]; // Max key length + uint8_t buf[1]; + uint32_t index = 0; + uint32_t bracket_count = 0; + int entries = 0; + bool quote = false; + bool mine = false; + bool deleted = false; + while (ifile.available()) { // Process file + ifile.read(buf, 1); + if (bracket_count > 1) { // Copy or skip old data + if (!mine) { + ofile.write(buf, 1); // Copy data + } + if (buf[0] == '}') { + bracket_count--; + } + } else { + if (buf[0] == '}') { // Last bracket + break; // End of file + } + else if (buf[0] == '{') { + bracket_count++; + if (bracket_count > 1) { // Skip first bracket + entries++; + } + } + else if (buf[0] == '"') { + quote ^= 1; + if (quote) { + index = 0; + } else { + buffer[index] = '\0'; // End of key name + mine = (!strcasecmp(buffer, key)); + if (mine) { + entries--; // Skip old data + deleted = true; + } else { + ofile.write((entries) ? (uint8_t*)",\"" : (uint8_t*)"{\"", 2); + ofile.write((uint8_t*)buffer, strlen(buffer)); + ofile.write((uint8_t*)"\":{", 3); + } + } + } + else { + buffer[index++] = buf[0]; // Add key name + if (index > sizeof(buffer) -2) { + break; // Key name too long + } + } + } + } + ifile.close(); + if (append) { + // Append new data + ofile.write((entries) ? (uint8_t*)"," : (uint8_t*)"{", 1); + ofile.write((uint8_t*)data +1, strlen(data) -1); + } else { + // Delete data + if (entries) { + ofile.write((uint8_t*)"}", 1); + } + } + ofile.close(); + + if (index > sizeof(buffer) -2) { + // No changes + ffsp->remove(bfname); + return false; // Error - Key name too long + } + ffsp->remove(filename); + ffsp->rename(bfname, filename); + if (!append) { + // Delete data + if (!entries) { + ffsp->remove(filename); + } + return deleted; // State - 0 = Not found, 1 = Deleted + } + return true; // State - Append success +} + +bool UfsJsonSettingsDelete(const char* key) { + // Delete: Input UserSet2 + // Output 0 = Not found, 1 = Deleted + return _UfsJsonSettingsUpdate(key); // State - 0 = Not found, 1 = Deleted +} + +bool UfsJsonSettingsWrite(const char* data) { + // Add new UserSet replacing present UserSet + // Input {"UserSet2":{"Param1":123,"Param2":"Text2"}} + // Output 0 = Error, 1 = Append success + + char filename[14]; + snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), 0); // /.drvset000 + if (!TfsFileExists(filename)) { + File ofile = ffsp->open(filename, "w"); + if (!ofile) { return false; } // Error - unable to open settings file + ofile.write((uint8_t*)data, strlen(data)); + ofile.close(); + return true; // State - Append success + } + return _UfsJsonSettingsUpdate(data); // State - 0 = Error, 1 = Append success +} + +String UfsJsonSettingsRead(const char* key) { + // Read: Input UserSet2 + // Output "" = Error, {"Param1":123,"Param2":"Text2"} = Data + + String data = ""; + char filename[14]; + snprintf_P(filename, sizeof(filename), PSTR(TASM_FILE_DRIVER), 0); // /.drvset000 + if (!TfsFileExists(filename)) { return data; } // Error - File not found + File file = ffsp->open(filename, "r"); + if (!file) { return data; } // Error - unable to open settings file + + Trim((char*)key); + char buffer[128]; + uint8_t buf[1] = { 0 }; + uint32_t index = 0; + uint32_t bracket_count = 0; + bool quote = false; + bool mine = false; + while (file.available()) { // Process file + file.read(buf, 1); + if (bracket_count > 1) { // Build JSON + if (mine) { + buffer[index++] = buf[0]; // Add key data + if (index > sizeof(buffer) -2) { + buffer[index] = '\0'; + data += buffer; // Add buffer to result + index = 0; + } + } + if (buf[0] == '}') { + bracket_count--; + if (1 == bracket_count) { + if (mine) { + break; // End of key data + } else { + index = 0; // End of data which is not mine + } + } + } + } else { + if (buf[0] == '}') { // Last bracket + index = 0; + break; // End of file - key not found + } + else if (buf[0] == '{') { + bracket_count++; + if (bracket_count > 1) { // Skip first bracket + index = 0; + buffer[index++] = buf[0]; // Start of key data + } + } + else if (buf[0] == '"') { + quote ^= 1; + if (quote) { + index = 0; + } else { + buffer[index] = '\0'; // End of key name + mine = (!strcasecmp(buffer, key)); + } + } + else { + buffer[index++] = buf[0]; // Add key name + if (index > sizeof(buffer) -2) { + index = 0; + break; // Key name too long + } + } + } + } + file.close(); + buffer[index] = '\0'; + data += buffer; + return data; // State - "" = Error, {"Param1":123,"Param2":"Text2"} = Data +} + /*********************************************************************************************\ * Commands \*********************************************************************************************/