Subversion Repositories ESP8266_P1_Meter

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 raymond 1
#ifndef CONFIG_UPGRADER_HPP
2
#define CONFIG_UPGRADER_HPP
3
 
4
#include <FS.h>
5
#include "SerialLog.h"
6
 
7
extern "C" {
8
#include "json/cJSON.h"
9
}
10
 
11
/**
12
 * @brief ConfigUpgrader handles migration from v1 (flat JSON) to v2 (hierarchical JSON)
13
 * Uses cJSON directly for reliable key iteration
14
 */
15
class ConfigUpgrader
16
{
17
public:
18
    ConfigUpgrader(fs::FS* filesystem, const char* configFile)
19
        : m_filesystem(filesystem), m_configFile(configFile) {}
20
 
21
    ~ConfigUpgrader() {}
22
 
23
    /**
24
     * @brief Check if upgrade is needed and perform it
25
     * @param outputFile Optional: save upgraded config to different file
26
     * @return true if upgrade was performed or file is already v2, false on error
27
     */
28
    bool upgrade(const char* outputFile = nullptr) {
29
        if (m_filesystem == nullptr || m_configFile == nullptr) {
30
            log_error("ConfigUpgrader: Invalid filesystem or config file");
31
            return false;
32
        }
33
 
34
        if (!m_filesystem->exists(m_configFile)) {
35
            log_debug("ConfigUpgrader: Config file does not exist, no upgrade needed");
36
            return true;
37
        }
38
 
39
        // Read config file
40
        File file = m_filesystem->open(m_configFile, "r");
41
        if (!file) {
42
            log_error("ConfigUpgrader: Failed to open config file");
43
            return false;
44
        }
45
 
46
        String content = file.readString();
47
        file.close();
48
 
49
        // Parse JSON with cJSON
50
        cJSON* oldJsonRoot = cJSON_Parse(content.c_str());
51
        if (!oldJsonRoot) {
52
            log_error("ConfigUpgrader: Failed to parse config JSON");
53
            return false;
54
        }
55
 
56
        // Check version
57
        cJSON* versionItem = cJSON_GetObjectItem(oldJsonRoot, "_version");
58
        if (versionItem && versionItem->valuestring && String(versionItem->valuestring).equals("2.0")) {
59
            log_debug("ConfigUpgrader: Config is already v2.0, no upgrade needed");
60
            cJSON_Delete(oldJsonRoot);
61
            return true;
62
        }
63
 
64
        log_info("ConfigUpgrader: Upgrading config from v1 to v2.0");
65
 
66
        // Perform upgrade
67
        String upgraded = upgradeFromV1(oldJsonRoot);
68
        cJSON_Delete(oldJsonRoot);
69
 
70
        if (upgraded.isEmpty()) {
71
            log_error("ConfigUpgrader: Upgrade failed");
72
            return false;
73
        }
74
 
75
        // Determine output file
76
        const char* targetFile = (outputFile != nullptr) ? outputFile : m_configFile;
77
 
78
        // Write upgraded config
79
        file = m_filesystem->open(targetFile, "w");
80
        if (!file) {
81
            log_error("ConfigUpgrader: Failed to open config file for writing");
82
            return false;
83
        }
84
 
85
        file.print(upgraded);
86
        file.close();
87
 
88
        log_info("ConfigUpgrader: Config upgraded and saved to %s", targetFile);
89
        return true;
90
    }
91
 
92
private:
93
    fs::FS* m_filesystem = nullptr;
94
    const char* m_configFile = nullptr;
95
 
96
    /**
97
     * @brief Generate valid ID from label
98
     */
99
    String generateId(const String& label) {
100
        String id = label;
101
        id.toLowerCase();
102
        id.replace(" ", "-");
103
 
104
        String cleaned;
105
        for (int i = 0; i < id.length(); i++) {
106
            char c = id[i];
107
            if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_') {
108
                cleaned += c;
109
            }
110
        }
111
 
112
        return cleaned.isEmpty() ? String("option-") : cleaned;
113
    }
114
 
115
    /**
116
     * @brief Escape special characters for JSON
117
     */
118
    String escapeJson(const String& input) {
119
        String result;
120
        for (int i = 0; i < input.length(); i++) {
121
            char c = input[i];
122
            switch (c) {
123
                case '"': result += "\\\""; break;
124
                case '\\': result += "\\\\"; break;
125
                case '\b': result += "\\b"; break;
126
                case '\f': result += "\\f"; break;
127
                case '\n': result += "\\n"; break;
128
                case '\r': result += "\\r"; break;
129
                case '\t': result += "\\t"; break;
130
                default:
131
                    if (c < 32) {
132
                        // Skip control characters
133
                    } else {
134
                        result += c;
135
                    }
136
            }
137
        }
138
        return result;
139
    }
140
 
141
    /**
142
     * @brief Upgrade JSON from v1 flat format to v2 hierarchical format
143
     */
144
    String upgradeFromV1(cJSON* oldRoot) {
145
        String result = "{\n";
146
 
147
        // Add version
148
        result += "  \"_version\": \"2.0\",\n";
149
 
150
        // Extract metadata
151
        result += "  \"_meta\": {\n";
152
 
153
        String pageTitle = "Configuration";
154
        String logoPath = "";
155
        double port = 80;
156
        String host = "myserver";
157
 
158
        cJSON* item = nullptr;
159
        if ((item = cJSON_GetObjectItem(oldRoot, "page-title")) && item->valuestring) {
160
            pageTitle = item->valuestring;
161
        }
162
        if ((item = cJSON_GetObjectItem(oldRoot, "img-logo")) && item->valuestring) {
163
            logoPath = item->valuestring;
164
        }
165
        if ((item = cJSON_GetObjectItem(oldRoot, "port")) && item->type == cJSON_Number) {
166
            port = item->valuedouble;
167
        }
168
        if ((item = cJSON_GetObjectItem(oldRoot, "host")) && item->valuestring) {
169
            host = item->valuestring;
170
        }
171
 
172
        result += "    \"app_title\": \"" + escapeJson(pageTitle) + "\",\n";
173
        if (!logoPath.isEmpty()) {
174
            result += "    \"logo\": \"" + escapeJson(logoPath) + "\",\n";
175
        }
176
        result += "    \"port\": " + String((long)port) + ",\n";
177
        result += "    \"host\": \"" + escapeJson(host) + "\"\n";
178
        result += "  },\n";
179
 
180
        // Add state (empty)
181
        result += "  \"_state\": {},\n";
182
 
183
        // Extract assets (CSS and JS only - HTML is handled as elements)
184
        std::vector<String> cssList;
185
        std::vector<String> jsList;
186
 
187
        for (cJSON* assetItem = oldRoot->child; assetItem; assetItem = assetItem->next) {
188
            String key = assetItem->string ? String(assetItem->string) : String("");
189
 
190
            if (key.indexOf("raw-css-") == 0 && assetItem->valuestring) {
191
                cssList.push_back(assetItem->valuestring);
192
            } else if ((key.indexOf("raw-javascript-") == 0 || key.indexOf("raw-js-") == 0) && assetItem->valuestring) {
193
                jsList.push_back(assetItem->valuestring);
194
            }
195
        }
196
 
197
        // Add assets section (CSS and JS only)
198
        result += "  \"_assets\": {\n";
199
        result += "    \"css\": [";
200
        for (size_t i = 0; i < cssList.size(); i++) {
201
            result += "\"" + escapeJson(cssList[i]) + "\"";
202
            if (i < cssList.size() - 1) result += ", ";
203
        }
204
        result += "],\n";
205
 
206
        result += "    \"js\": [";
207
        for (size_t i = 0; i < jsList.size(); i++) {
208
            result += "\"" + escapeJson(jsList[i]) + "\"";
209
            if (i < jsList.size() - 1) result += ", ";
210
        }
211
        result += "]\n";
212
        result += "  },\n";
213
 
214
        // Add sections
215
        result += "  \"sections\": [\n";
216
        result += upgradeToSections(oldRoot);
217
        result += "  ]\n";
218
        result += "}\n";
219
 
220
        return result;
221
    }
222
 
223
    /**
224
     * @brief Convert v1 elements to v2 sections
225
     */
226
    String upgradeToSections(cJSON* oldRoot) {
227
        String result;
228
        String currentSectionId = "general-options";
229
        String currentSectionTitle = "Options";
230
        std::vector<String> currentElements;
231
        bool firstSection = true;
232
 
233
        // Iterate through all top-level keys
234
        for (cJSON* item = oldRoot->child; item; item = item->next) {
235
            String key = item->string ? String(item->string) : String("");
236
            if (key.isEmpty()) continue;
237
 
238
            // Skip system keys
239
            if (key.equals("_version") || key.equals("_meta") || 
240
                key.equals("_state") || key.equals("_assets") ||
241
                key.equals("page-title") || key.equals("img-logo") ||
242
                key.equals("port") || key.equals("host")) {
243
                continue;
244
            }
245
 
246
            // Skip raw-css and raw-javascript (they all go to _assets)
247
            if ((key.indexOf("raw-css-") == 0 || key.indexOf("raw-javascript-") == 0 || 
248
                 key.indexOf("raw-js-") == 0) && 
249
                key.indexOf("raw-html-") != 0) {
250
                continue;
251
            }
252
 
253
            // Handle section titles
254
            if (key.indexOf("param-box") == 0) {
255
                // Save current section if has elements
256
                if (currentElements.size() > 0) {
257
                    if (!firstSection) result += ",\n";
258
                    result += buildSection(currentSectionId, currentSectionTitle, currentElements);
259
                    firstSection = false;
260
                }
261
 
262
                // Start new section
263
                String sectionTitle = item->valuestring ? String(item->valuestring) : String("Section");
264
                currentSectionId = generateId(sectionTitle);
265
                currentSectionTitle = sectionTitle;
266
                currentElements.clear();
267
                continue;
268
            }
269
 
270
            // Skip image and name keys
271
            if (key.indexOf("img-") == 0 || key.indexOf("name-") == 0) {
272
                continue;
273
            }
274
 
275
            // Convert option
276
            String elemJson = convertV1Option(key, item);
277
            if (!elemJson.isEmpty()) {
278
                currentElements.push_back(elemJson);
279
            }
280
        }
281
 
282
        // Save last section
283
        if (currentElements.size() > 0) {
284
            if (!firstSection) result += ",\n";
285
            result += buildSection(currentSectionId, currentSectionTitle, currentElements);
286
            result += "\n";
287
        }
288
 
289
        return result;
290
    }
291
 
292
    /**
293
     * @brief Build a section JSON block
294
     */
295
    String buildSection(const String& id, const String& title, const std::vector<String>& elements) {
296
        String result = "    {\n";
297
        result += "      \"title\": \"" + escapeJson(title) + "\",\n";
298
        result += "      \"elements\": [\n";
299
        for (size_t i = 0; i < elements.size(); i++) {
300
            result += elements[i];
301
            if (i < elements.size() - 1) result += ",";
302
            result += "\n";
303
        }
304
        result += "      ]\n";
305
        result += "    }";
306
        return result;
307
    }
308
 
309
    /**
310
     * @brief Convert a single v1 option to v2 element
311
     */
312
    String convertV1Option(const String& key, cJSON* item) {
313
        String result = "        {\n";
314
        result += "          \"label\": \"" + escapeJson(key) + "\",\n";
315
 
316
        // Check if it's an object with metadata
317
        if (item->type == cJSON_Object) {
318
            cJSON* typeItem = cJSON_GetObjectItem(item, "type");
319
            String typeStr = (typeItem && typeItem->valuestring) ? String(typeItem->valuestring) : String("");
320
 
321
            if (typeStr.equals("slider")) {
322
                result += "          \"type\": \"slider\",\n";
323
 
324
                double value = 0, min = 0, max = 100, step = 1;
325
                cJSON* v = cJSON_GetObjectItem(item, "value");
326
                if (v && v->type == cJSON_Number) value = v->valuedouble;
327
                v = cJSON_GetObjectItem(item, "min");
328
                if (v && v->type == cJSON_Number) min = v->valuedouble;
329
                v = cJSON_GetObjectItem(item, "max");
330
                if (v && v->type == cJSON_Number) max = v->valuedouble;
331
                v = cJSON_GetObjectItem(item, "step");
332
                if (v && v->type == cJSON_Number) step = v->valuedouble;
333
 
334
                result += "          \"value\": " + String(value) + ",\n";
335
                result += "          \"min\": " + String(min) + ",\n";
336
                result += "          \"max\": " + String(max) + ",\n";
337
                result += "          \"step\": " + String(step) + "\n";
338
            } 
339
            else if (typeStr.equals("number")) {
340
                result += "          \"type\": \"number\",\n";
341
 
342
                double value = 0, min = -3.4e38, max = 3.4e38, step = 1;
343
                cJSON* v = cJSON_GetObjectItem(item, "value");
344
                if (v && v->type == cJSON_Number) value = v->valuedouble;
345
                v = cJSON_GetObjectItem(item, "min");
346
                if (v && v->type == cJSON_Number) min = v->valuedouble;
347
                v = cJSON_GetObjectItem(item, "max");
348
                if (v && v->type == cJSON_Number) max = v->valuedouble;
349
                v = cJSON_GetObjectItem(item, "step");
350
                if (v && v->type == cJSON_Number) step = v->valuedouble;
351
 
352
                result += "          \"value\": " + String(value) + ",\n";
353
                result += "          \"min\": " + String(min) + ",\n";
354
                result += "          \"max\": " + String(max) + ",\n";
355
                result += "          \"step\": " + String(step) + "\n";
356
            }
357
            else if (cJSON_GetObjectItem(item, "selected") != nullptr) {
358
                // Dropdown
359
                result += "          \"type\": \"select\",\n";
360
 
361
                cJSON* selected = cJSON_GetObjectItem(item, "selected");
362
                if (selected && selected->valuestring) {
363
                    result += "          \"value\": \"" + escapeJson(selected->valuestring) + "\",\n";
364
                }
365
 
366
                // Extract options array
367
                result += "          \"options\": [";
368
                cJSON* valuesArray = cJSON_GetObjectItem(item, "values");
369
                if (valuesArray && valuesArray->type == cJSON_Array) {
370
                    bool firstOption = true;
371
                    for (cJSON* optItem = valuesArray->child; optItem; optItem = optItem->next) {
372
                        if (optItem->valuestring) {
373
                            if (!firstOption) result += ", ";
374
                            result += "\"" + escapeJson(optItem->valuestring) + "\"";
375
                            firstOption = false;
376
                        }
377
                    }
378
                }
379
                result += "]\n";
380
            }
381
            else {
382
                result += "          \"type\": \"text\",\n";
383
                cJSON* val = cJSON_GetObjectItem(item, "value");
384
                if (val && val->valuestring) {
385
                    result += "          \"value\": \"" + escapeJson(val->valuestring) + "\"\n";
386
                }
387
            }
388
        } 
389
        else {
390
            // Primitive value - infer type
391
            if (item->type == cJSON_True || item->type == cJSON_False) {
392
                result += "          \"type\": \"boolean\",\n";
393
                result += "          \"value\": " + String(item->type == cJSON_True ? "true" : "false") + "\n";
394
            } 
395
            else if (item->type == cJSON_Number) {
396
                result += "          \"type\": \"number\",\n";
397
                result += "          \"value\": " + String(item->valuedouble) + "\n";
398
            } 
399
            else if (item->type == cJSON_String && item->valuestring) {
400
                result += "          \"type\": \"text\",\n";
401
                result += "          \"value\": \"" + escapeJson(item->valuestring) + "\"\n";
402
            }
403
            else {
404
                result += "          \"type\": \"text\",\n";
405
                result += "          \"value\": \"\"\n";
406
            }
407
        }
408
 
409
        result += "        }";
410
        return result;
411
    }
412
};
413
 
414
#endif // CONFIG_UPGRADER_HPP