diff --git a/.github/workflows/pio_build.yml b/.github/workflows/pio_build.yml index 9abb8d6..402829e 100644 --- a/.github/workflows/pio_build.yml +++ b/.github/workflows/pio_build.yml @@ -37,18 +37,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: PIP Cache - id: cache-pip - uses: actions/cache@v4 - env: - cache-name: cache-pip-pkgs - with: - path: | - ~/.cache/pip - key: ${{ runner.os }}-pip-${{env.cache-name}} - restore-keys: | - ${{ runner.os }}-pio- - ${{ runner.os }}- - name: Set up Python uses: actions/setup-python@v5 with: @@ -57,7 +45,19 @@ jobs: run: | python -m pip install --upgrade pip pip install --upgrade platformio + #platformio run -t buildfs #platformio pkg update + - name: Patch Platformio + run: | + pwd + echo "REPO_NAME=$(basename ${{ github.repository }})" >> $GITHUB_ENV + cd $(dirname $(which pio) ) + cd ../ + echo $(find -type d -name site-packages) + cd $(find -type d -name site-packages) + pwd + git apply --verbose $GITHUB_WORKSPACE/extra/0001-LDF-refresh-lib-dependency-after-recursive-search.patch + cd $GITHUB_WORKSPACE - name: Run PlatformIO run: | pio run -e espem -e espem_debug diff --git a/.gitignore b/.gitignore index 68189d1..1c7f74e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .vscode config.json lnk +data.* drafts *.bin *.orig diff --git a/data/css/menu-dark.webp b/data/css/menu-dark.webp new file mode 100644 index 0000000..182e533 Binary files /dev/null and b/data/css/menu-dark.webp differ diff --git a/data/css/menu-light.webp b/data/css/menu-light.webp new file mode 100644 index 0000000..167ed83 Binary files /dev/null and b/data/css/menu-light.webp differ diff --git a/data/css/pure.css.gz b/data/css/pure.css.gz index 4076447..4c98d30 100644 Binary files a/data/css/pure.css.gz and b/data/css/pure.css.gz differ diff --git a/data/css/style.css.gz b/data/css/style.css.gz index 437daee..17c816b 100644 Binary files a/data/css/style.css.gz and b/data/css/style.css.gz differ diff --git a/data/css/style_dark.css.gz b/data/css/style_dark.css.gz index fe88b65..e8fc54f 100644 Binary files a/data/css/style_dark.css.gz and b/data/css/style_dark.css.gz differ diff --git a/data/css/style_light.css.gz b/data/css/style_light.css.gz index c480dba..c70ab2d 100644 Binary files a/data/css/style_light.css.gz and b/data/css/style_light.css.gz differ diff --git a/data/index.html.gz b/data/index.html.gz index f499ec7..1784c21 100644 Binary files a/data/index.html.gz and b/data/index.html.gz differ diff --git a/data/js/embui.js.gz b/data/js/embui.js.gz index 78fe95e..1b6c1f8 100644 Binary files a/data/js/embui.js.gz and b/data/js/embui.js.gz differ diff --git a/data/js/espem.js.gz b/data/js/espem.js.gz index f57369e..7855bd2 100644 Binary files a/data/js/espem.js.gz and b/data/js/espem.js.gz differ diff --git a/data/js/espem.ui.json.gz b/data/js/espem.ui.json.gz index 0fb494c..31c49b5 100644 Binary files a/data/js/espem.ui.json.gz and b/data/js/espem.ui.json.gz differ diff --git a/data/js/lodash.custom.js.gz b/data/js/lodash.custom.js.gz index 342fda6..3f3acec 100644 Binary files a/data/js/lodash.custom.js.gz and b/data/js/lodash.custom.js.gz differ diff --git a/data/js/ui_embui.i18n.json.gz b/data/js/ui_embui.i18n.json.gz new file mode 100644 index 0000000..b5309da Binary files /dev/null and b/data/js/ui_embui.i18n.json.gz differ diff --git a/data/js/ui_embui.json.gz b/data/js/ui_embui.json.gz new file mode 100644 index 0000000..725e61b Binary files /dev/null and b/data/js/ui_embui.json.gz differ diff --git a/data/js/ui_embui.lang.json.gz b/data/js/ui_embui.lang.json.gz new file mode 100644 index 0000000..da19bfa Binary files /dev/null and b/data/js/ui_embui.lang.json.gz differ diff --git a/data/js/ui_sys.json.gz b/data/js/ui_sys.json.gz deleted file mode 100644 index deda8fd..0000000 Binary files a/data/js/ui_sys.json.gz and /dev/null differ diff --git a/espem/config.h b/espem/config.h deleted file mode 100644 index 76f561b..0000000 --- a/espem/config.h +++ /dev/null @@ -1,24 +0,0 @@ -// Default config options -// do NOT change anything here, copy 'config.h' into 'user_config.h' and change you options there - -#pragma once - -#if defined __has_include -# if __has_include("user_config.h") -# include "user_config.h" -# endif -#endif - - -#define FW_NAME "espem" - -// LOG macro's -#if defined(LOG) -#undef LOG -#endif - -#if defined(ESPEM_DEBUG) - #define LOG(func, ...) ESPEM_DEBUG.func(__VA_ARGS__) -#else - #define LOG(func, ...) ; -#endif diff --git a/espem/espem.cpp b/espem/espem.cpp index ca73bea..759e04e 100644 --- a/espem/espem.cpp +++ b/espem/espem.cpp @@ -2,26 +2,23 @@ * A code for ESP32 based boards to interface with PeaceFair PZEM PowerMeters * It can poll/collect PowerMeter data and provide it for futher processing in text/json format * - * (c) Emil Muratov 2018-2022 https://github.com/vortigont/espem + * (c) Emil Muratov 2018-2024 https://github.com/vortigont/espem * */ #include "espem.h" #include "EmbUI.h" // EmbUI framework +#include "log.h" + -#define MAX_FREE_MEM_BLK ESP.getMaxAllocHeap() -#define PUB_JSSIZE 1024 // sprintf template for json sampling data #define JSON_SMPL_LEN 85 // {"t":1615496537000,"U":229.50,"I":1.47,"P":1216,"W":5811338,"hz":50.0,"pF":0.64}, -static const char PGsmpljsontpl[] PROGMEM = "{\"t\":%u000,\"U\":%.2f,\"I\":%.2f,\"P\":%.0f,\"W\":%.0f,\"hz\":%.1f,\"pF\":%.2f},"; -static const char PGdatajsontpl[] PROGMEM = "{\"age\":%llu,\"U\":%.1f,\"I\":%.2f,\"P\":%.0f,\"W\":%.0f,\"hz\":%.1f,\"pF\":%.2f}"; +static constexpr const char* PGsmpljsontpl = "{\"t\":%u000,\"U\":%.2f,\"I\":%.2f,\"P\":%.0f,\"W\":%.0f,\"hz\":%.1f,\"pF\":%.2f},"; +static constexpr const char* PGdatajsontpl = "{\"age\":%llu,\"U\":%.1f,\"I\":%.2f,\"P\":%.0f,\"W\":%.0f,\"hz\":%.1f,\"pF\":%.2f}"; // HTTP responce messages -static const char PGsmpld[] = "Metrics collector disabled"; -static const char PGdre[] = "Data read error"; -static const char PGacao[] = "Access-Control-Allow-Origin"; -static const char* PGmimetxt = "text/plain"; -//static const char* PGmimehtml = "text/html; charset=utf-8"; +static constexpr const char* PGsmpld = "Metrics collector disabled"; +static constexpr const char* PGdre = "Data read error"; using namespace pzmbus; // use general pzem abstractions @@ -37,7 +34,7 @@ class FrameSendMQTTRaw : public FrameSendMQTT { }; bool Espem::begin(const uart_port_t p, int rx, int tx){ - LOG(printf, "espem.begin: port: %d, rx_pin: %d, tx_pin:%d\n", p, rx, tx); + LOGI(C_espem, printf, "espem.begin: port: %d, rx_pin: %d, tx_pin:%d\n", p, rx, tx); // let's make our begin idempotent ) if (qport){ @@ -74,9 +71,9 @@ bool Espem::begin(const uart_port_t p, int rx, int tx){ if (pz->autopoll(true)){ t_uiupdater.restartDelayed(); - LOG(println, "Autopolling enabled"); + LOGI(C_espem, println, "Autopolling enabled"); } else { - LOG(println, "Sorry, can't autopoll somehow :("); + LOGE(C_espem, println, "Error on enabling autopolling!"); } embui.server.on(PSTR("/getdata"), HTTP_GET, [this](AsyncWebServerRequest *request){ @@ -123,12 +120,12 @@ String& Espem::mktxtdata ( String& txtdata) { // compat method for v 1.x cacti scripts void Espem::wpmdata(AsyncWebServerRequest *request) { if ( !ds.getTSsize(1) ) { - request->send(503, PGmimetxt, PGdre ); + request->send(503, asyncsrv::T_text_plain, PGdre ); return; } String data; - request->send(200, PGmimetxt, mktxtdata(data) ); + request->send(200, asyncsrv::T_text_plain, mktxtdata(data) ); } @@ -147,7 +144,7 @@ void Espem::wdatareply(AsyncWebServerRequest *request){ m->asFloat(meter_t::frq), m->asFloat(meter_t::pf) ); - request->send(200, PGmimejson, buffer ); + request->send(200, asyncsrv::T_application_json, buffer ); } @@ -162,7 +159,7 @@ void DataStorage::wsamples(AsyncWebServerRequest *request) { // check if there is any sampled data if ( !getTSsize(id) ) { - request->send(503, PGmimejson, "[]"); + request->send(503, asyncsrv::T_application_json, "[]"); return; } @@ -180,7 +177,7 @@ void DataStorage::wsamples(AsyncWebServerRequest *request) { const auto ts = getTS(id); if (!ts) - request->send(503, PGmimejson, "[]"); + request->send(503, asyncsrv::T_application_json, "[]"); auto iter = ts->cbegin(); // get const iterator @@ -188,9 +185,9 @@ void DataStorage::wsamples(AsyncWebServerRequest *request) { if (cnt > 0 && cnt < ts->getSize()) iter += ts->getSize() - cnt; // offset iterator to the last cnt elements - LOG(printf, "TimeSeries buffer has %d items, scntr: %d\n", ts->getSize(), cnt); + LOGV(C_espem, printf, "TimeSeries buffer has %d items, scntr: %d\n", ts->getSize(), cnt); - AsyncWebServerResponse* response = request->beginChunkedResponse(PGmimejson, + AsyncWebServerResponse* response = request->beginChunkedResponse(asyncsrv::T_application_json, [this, iter, ts](uint8_t* buffer, size_t buffsize, size_t index) mutable -> size_t { // If provided bufer is not large enough to fit 1 sample chunk, than I'm just sending @@ -224,18 +221,18 @@ void DataStorage::wsamples(AsyncWebServerRequest *request) { m.asFloat(meter_t::pf) ); } else { - LOG(println, "SMLP pointer is null"); + LOGW(C_espem, println, "SMLP pointer is null"); } if (++iter == ts->cend()) buffer[len-1] = 0x5d; // ASCII ']' implaced over last comma } - LOG(printf, "Sending timeseries JSON, buffer %d/%d, items left: %d\n", len, buffsize, ts->cend() - iter); + LOGV(C_espem, printf, "Sending timeseries JSON, buffer %d/%d, items left: %d\n", len, buffsize, ts->cend() - iter); return len; }); - response->addHeader(PGacao, "*"); // CORS header + response->addHeader(asyncsrv::T_CORS_ACAO, "*"); // CORS header request->send(response); } @@ -246,20 +243,19 @@ void Espem::wspublish(){ const auto m = pz->getMetricsPZ004(); - DynamicJsonDocument doc(PUB_JSSIZE); - JsonObject obj = doc.to(); - doc["stale"] = pz->getState()->dataStale(); - doc["age"] = pz->getState()->dataAge(); - doc["U"] = m->voltage; - doc["I"] = m->current; - doc["P"] = m->power; - doc["W"] = m->energy + ds.getEnergyOffset(); - doc["Pf"] = m->pf; - doc["freq"] = m->freq; - - Interface interf(&embui.feeders, 128); - interf.json_frame(C_espem); - interf.jobject(doc, true); + Interface interf(&embui.feeders); + interf.json_frame(C_espem, C_sample); + + JsonObject obj = interf.json_object_create(); + obj["stale"] = pz->getState()->dataStale(); + obj["age"] = pz->getState()->dataAge(); + obj["U"] = m->voltage; + obj["I"] = m->current; + obj["P"] = m->power; + obj["W"] = m->energy + ds.getEnergyOffset(); + obj["Pf"] = m->pf; + obj["freq"] = m->freq; + interf.json_frame_flush(); } @@ -285,30 +281,30 @@ void DataStorage::reset(){ tsids.clear(); uint8_t a; - a = addTS(embui.paramVariant(V_TS_T1_CNT), time(nullptr), embui.paramVariant(V_TS_T1_INT), "Tier 1", 1); + a = addTS(embui.getConfig()[V_TS_T1_CNT] | TS_T1_CNT, time(nullptr), embui.getConfig()[V_TS_T1_INT] | TS_T1_INTERVAL, "Tier 1", 1); tsids.push_back(a); - //LOG(printf, "Add TS: %d\n", a); + LOGD(C_espem, printf, "Add TS: %d\n", a); - a = addTS(embui.paramVariant(V_TS_T2_CNT), time(nullptr), embui.paramVariant(V_TS_T2_INT), "Tier 2", 2); + a = addTS(embui.getConfig()[V_TS_T2_CNT] | TS_T2_CNT, time(nullptr), embui.getConfig()[V_TS_T2_INT] | TS_T2_INTERVAL, "Tier 2", 2); tsids.push_back(a); - //LOG(printf, "Add TS: %d\n", a); + LOGD(C_espem, printf, "Add TS: %d\n", a); - a = addTS(embui.paramVariant(V_TS_T3_CNT), time(nullptr), embui.paramVariant(V_TS_T3_INT), "Tier 3", 3); + a = addTS(embui.getConfig()[V_TS_T3_CNT] | TS_T3_CNT, time(nullptr), embui.getConfig()[V_TS_T3_INT] | TS_T3_INTERVAL, "Tier 3", 3); tsids.push_back(a); - //LOG(printf, "Add TS: %d\n", a); + LOG(printf, "Add TS: %d\n", a); - LOG(println, "Setup TimeSeries DB:"); + LOGI(C_espem, println, "Setup TimeSeries DB:"); LOG_CALL( for ( auto i : tsids ){ auto t = getTS(i); if (t){ - LOG(printf, "%s: size:%d, interval:%u, mem:%u\n", t->getDescr(), t->capacity, t->getInterval(), t->capacity * sizeof(pz004::metrics)); + LOGI(C_espem, printf, "%s: size:%d, interval:%u, mem:%u\n", t->getDescr(), t->capacity, t->getInterval(), t->capacity * sizeof(pz004::metrics)); } } ) - LOG(printf, "SRAM: heap %u, free %u\n", ESP.getHeapSize(), ESP.getFreeHeap()); - LOG(printf, "SPI-RAM: size %u, free %u\n", ESP.getPsramSize(), ESP.getFreePsram()); + LOGI(C_espem, printf, "SRAM: heap %u, free %u\n", ESP.getHeapSize(), ESP.getFreeHeap()); + LOGI(C_espem, printf, "SPI-RAM: size %u, free %u\n", ESP.getPsramSize(), ESP.getFreePsram()); } mcstate_t Espem::set_collector_state(mcstate_t state){ @@ -351,7 +347,7 @@ mcstate_t Espem::set_collector_state(mcstate_t state){ void msgdebug(uint8_t id, const RX_msg* m){ - Serial.printf("\nCallback triggered for PZEM ID: %d\n", id); + LOGD("DBG", printf, "\nCallback triggered for PZEM ID: %d\n", id); /* It is also possible to work directly on a raw data from PZEM diff --git a/espem/espem.h b/espem/espem.h index 4d2ff3f..190a35c 100644 --- a/espem/espem.h +++ b/espem/espem.h @@ -10,9 +10,9 @@ #include "main.h" #include "pzem_edl.hpp" #include "timeseries.hpp" - // Tasker object from EmbUI #include "ts.h" +#include "ESPAsyncWebServer.h" // Defaults #ifndef DEFAULT_WS_UPD_RATE diff --git a/espem/interface.cpp b/espem/interface.cpp index 621909d..f909085 100644 --- a/espem/interface.cpp +++ b/espem/interface.cpp @@ -3,49 +3,46 @@ #include "interface.h" #include "ui_i18n.h" // localized GUI text-strings -// статический класс с готовыми формами для базовых системных натсроек +// EmbUI's basic interface #include "basicui.h" #define MAX_UI_UPDATE_RATE 30 +#define DS_ENTRY_SIZE 28 +// our espem object extern Espem *espem; -static const char* chart_css = "graphwide"; +static constexpr const char* WebUI = "WebUI"; +static constexpr const char* chart_css = "graphwide"; // variable that holds TS id which is currently displayed at Web UI unsigned power_chart_id{1}; // forward declarations -void ui_page_espem(Interface *interf, const JsonObject *data, const char* action); +void ui_page_espem(Interface *interf, JsonObjectConst data, const char* action); /** - * Headlile section - * this is an overriden weak method that builds our WebUI from the top + * Main ESPEM WebUI page * == * Головная секция - * переопределенный метод фреймфорка, который начинает строить корень нашего WebUI * */ -void ui_page_main(Interface *interf, const JsonObject *data, const char* action){ +void ui_page_main(Interface *interf, JsonObjectConst data, const char* action){ interf->json_frame_interface(); // application manifest - interf->json_section_manifest(C_DICT[lang][CD::ESPEM_H], embui.macid(), ESPEM_JSAPI_VERSION, FW_VERSION_STRING); // HEADLINE for WebUI - interf->json_section_end(); // json_section_manifest - - // load uidata objects - interf->json_section_uidata(); - interf->uidata_xload(C_espem_ui, "js/espem.ui.json", false, ESPEM_UI_VERSION); + interf->json_section_manifest(C_EspEM, embui.macid(), ESPEM_JSAPI_VERSION, FW_VERSION_STRING); // HEADLINE for WebUI interf->json_section_end(); - block_menu(interf); // Строим UI блок с меню выбора других секций + block_menu(interf); // Build UI Menu block interf->json_frame_flush(); // send frame if((WiFi.getMode() & WIFI_MODE_STA)){ // если контроллер не подключен к внешней AP, сразу открываем вкладку с настройками WiFi - ui_page_espem(interf, nullptr, action); // construct main page + ui_page_espem(interf, {}, action); // construct main page } else { - basicui::page_settings_netw(interf, nullptr, NULL); + // Open WiFi setup page + basicui::page_settings_netw(interf, {}, NULL); } } @@ -58,7 +55,7 @@ void block_menu(Interface *interf){ interf->json_section_menu(); // открываем секцию "меню" interf->option(A_ui_page_espem, C_DICT[lang][CD::ESPEM_DB]); // пункт меню "ESPEM Info" interf->option(A_ui_page_espem_setup, C_DICT[lang][CD::ESPEMSet]); // пункт меню "ESPEM Setup" - interf->option(A_ui_page_dataexport, "ESPEM DataExport"); // пункт меню "ESPEM Data Export" + interf->option(A_ui_page_dataexport, "DataExport"); // пункт меню "ESPEM Data Export" /** * добавляем в меню пункт - настройки, @@ -75,16 +72,14 @@ void block_menu(Interface *interf){ */ void ui_frame_mkchart(Interface *interf){ interf->json_frame_jscall(C_mkchart); - StaticJsonDocument<128> doc; - JsonObject params = doc.to(); // parameters for charts - params[P_id] = C_gsmini; - params[C_tier] = power_chart_id; - auto ts = espem->ds.getTS(power_chart_id); - // check if requested TimeSeries exist - if (ts) - params["interval"] = ts->getInterval(); - params[C_scnt] = embui.paramVariant(V_SMPLCNT).as(); // espem->ds.getTScap(power_chart_id); // samples counter - interf->jobject(params, true); + JsonObject params(interf->json_object_create()); + params[P_id] = C_smpchart; + params[C_tier] = power_chart_id; + auto ts = espem->ds.getTS(power_chart_id); + // check if requested TimeSeries exist + if (ts) + params["interval"] = ts->getInterval(); + params[C_scnt] = embui.getConfig()[V_SMPLCNT].as() | espem->ds.getTScap(power_chart_id); // espem->ds.getTScap(power_chart_id); // samples counter interf->json_frame_flush(); // flush frame } @@ -104,80 +99,96 @@ void ui_block_chart_ctrls(Interface *interf){ interf->json_section_end(); // end select drop-down // slider for the amount of metric samples to be plotted on a chart - interf->range(A_SMPLCNT, embui.paramVariant(V_SMPLCNT).as(), 0, (int)espem->ds.getTScap(power_chart_id), 10, C_DICT[lang][CD::MScale], true); + //espem->ds.getTS(i+1) + interf->range( + A_SMPLCNT, // id + embui.getConfig()[V_SMPLCNT].as() | 50, // value + 0, // min + espem->ds.getTScap(power_chart_id), // max + espem->ds.getTScap(power_chart_id)/100, // step + C_DICT[lang][CD::MScale], true // label + ); interf->json_section_end(); // end of line } /** - * This code builds UI section with dashboard + * @brief send values for power chart controls + * + * @param interf + */ +void ui_block_chart_ctrls_values(Interface *interf){ + interf->json_frame_value(); + interf->value(A_TS_TIER, power_chart_id); + interf->value(A_SMPLCNT, embui.getConfig()[V_SMPLCNT].as() | 50); + interf->json_frame_flush(); +} + +/** + * This code builds UI page with dashboard + * @note called from a ui_page_main() * */ -void ui_page_espem(Interface *interf, const JsonObject *data, const char* action){ +void ui_page_espem(Interface *interf, JsonObjectConst data, const char* action){ interf->json_frame_interface(); - interf->json_section_main(A_ui_page_espem, C_DICT[lang][CD::ESPEM_H]); - - interf->json_section_line(); // "Live controls" - interf->checkbox(A_EPOLLENA, (bool)espem->get_uirate(), "Live update", true); // Meter poller status - // UI update rate range slider - interf->range(A_UI_UPDRT, embui.paramVariant(V_UI_UPDRT).as(), 0, MAX_UI_UPDATE_RATE, 1, "UI update rate, sec", true); - interf->json_section_end(); // end of line - - // Plain values display - interf->json_section_line(); // "Live controls" - auto *m = espem->pz->getMetricsPZ004(); - // Widgets & left side menu - // id, type, value, label, param - interf->display("pwr", m->power/10 ); // Power - interf->display("cur", m->asFloat(pzmbus::meter_t::cur)); // Current - interf->display("enrg", m->energy/1000); // Energy - interf->json_section_end(); // end of line + interf->json_section_uidata(); + interf->uidata_pick("espem.ui.pages.main"); + interf->json_section_end(); - interf->json_section_line(); - // id, value, label, param - interf->jscall("gaugeV", C_mkgauge, C_DICT[lang][CD::Voltage], chart_css); // Voltage gauge - interf->jscall("gaugePF", C_mkgauge, C_DICT[lang][CD::PowerF], chart_css); // Power Factor - interf->json_section_end(); // end of line + //interf->spacer("Power chart spacer"); - interf->spacer("Power chart"); + // draw block with chart controls ui_block_chart_ctrls(interf); - // empty div placeholder for TimeSeries Power chart - interf->jscall(C_gsmini, P_EMPTY, P_EMPTY, chart_css); + // div placeholder for TimeSeries Power chart + //interf->jscall(C_gsmini, P_EMPTY, P_EMPTY, chart_css); + //interf->div(C_gsmini, P_html, P_EMPTY, P_EMPTY, chart_css); + interf->json_section_begin(C_minichart); + interf->div(C_smpchart, "chart", P_EMPTY, "Power Sampling", chart_css); + interf->spacer("Power Chart"); - interf->json_frame_flush(); // flush frame + // send values for controls + auto m = espem->pz->getMetricsPZ004(); + interf->json_frame_value(); + interf->value(A_EPOLLENA, (bool)espem->get_uirate()); + interf->value(A_UI_UPDRT, embui.getConfig()[V_UI_UPDRT].as() | DEFAULT_WS_UPD_RATE); // call js function to build power chart ui_frame_mkchart(interf); + } -// Create Additional buttons on "Settings" page -void user_settings_frame(Interface *interf, const JsonObject *data, const char* action){ + +/** + * @brief A callback function + * Creates Additional buttons on EmbUI's "Settings" page + */ +void block_user_settings(Interface *interf, JsonObjectConst data, const char* action){ interf->button(button_t::generic, A_ui_page_espem_setup, "ESPEM"); } /** - * ESPEM options setup + * ESPEM configuration options page * */ -void block_page_espemset(Interface *interf, const JsonObject *data, const char* action){ +void block_page_espemset(Interface *interf, JsonObjectConst data, const char* action){ interf->json_frame_interface(); interf->json_section_uidata(); - interf->uidata_pick("espem.ui.settings.cfg"); + interf->uidata_pick("espem.ui.pages.settings"); interf->json_frame_flush(); interf->json_frame_value(); - interf->value(V_UART, embui.paramVariant(V_UART).as()); // Uart port - interf->value(V_RX, embui.paramVariant(V_RX).as()); - interf->value(V_TX, embui.paramVariant(V_TX).as()); + interf->value(V_UART, embui.getConfig()[V_UART].as() | UART_NUM_1); // Uart port + interf->value(V_RX, embui.getConfig()[V_RX].as()); + interf->value(V_TX, embui.getConfig()[V_TX].as()); interf->value(V_EOFFSET, espem->ds.getEnergyOffset()); // TimeSeries capacity - interf->value(V_TS_T1_CNT, embui.paramVariant(V_TS_T1_CNT).as()); - interf->value(V_TS_T1_INT, embui.paramVariant(V_TS_T1_INT).as()); - interf->value(V_TS_T2_CNT, embui.paramVariant(V_TS_T2_CNT).as()); - interf->value(V_TS_T2_INT, embui.paramVariant(V_TS_T2_INT).as()); - interf->value(V_TS_T3_CNT, embui.paramVariant(V_TS_T3_CNT).as()); - interf->value(V_TS_T3_INT, embui.paramVariant(V_TS_T3_INT).as()); + interf->value(V_TS_T1_CNT, embui.getConfig()[V_TS_T1_CNT].as()); + interf->value(V_TS_T1_INT, embui.getConfig()[V_TS_T1_INT].as()); + interf->value(V_TS_T2_CNT, embui.getConfig()[V_TS_T2_CNT].as()); + interf->value(V_TS_T2_INT, embui.getConfig()[V_TS_T2_INT].as()); + interf->value(V_TS_T3_CNT, embui.getConfig()[V_TS_T3_CNT].as()); + interf->value(V_TS_T3_INT, embui.getConfig()[V_TS_T3_INT].as()); // collector state interf->value(A_ECOLLECTORSTATE, (uint8_t)espem->get_collector_state()); interf->json_frame_flush(); @@ -189,73 +200,22 @@ void block_page_espemset(Interface *interf, const JsonObject *data, const char* for ( unsigned i=1; i!=4; ++i ){ char buff[64]; char key[8]; - std::snprintf(buff, 64, "Used: %hu/%hu, %u kib", espem->ds.getTSsize(i), espem->ds.getTScap(i), espem->ds.getTScap(i) * 28 / 1024); // sizeof(pz004::metric) + std::snprintf(buff, 64, "Used: %hu/%hu, %u KiB", espem->ds.getTSsize(i), espem->ds.getTScap(i), espem->ds.getTScap(i) * DS_ENTRY_SIZE / 1024); // sizeof(pz004::metric) std::snprintf(key,8, "t%umem", i); interf->constant(std::string_view(key), std::string_view(buff)); // capacity and memory usage } interf->json_frame_flush(); - - -/* - // replacing page with a new one with settings - interf->json_section_main(A_ui_page_espem_setup, C_DICT[lang][CD::ESPEMSet]); - - interf->json_section_begin(A_SET_UART); - interf->json_section_line(); - interf->number_constrained(V_UART, embui.paramVariant(V_UART).as(), "Uart port", 1, 0, SOC_UART_NUM); - interf->number_constrained(V_RX, embui.paramVariant(V_RX).as(), "RX pin (-1 default)", 1, -1, NUM_OUPUT_PINS); - interf->number_constrained(V_TX, embui.paramVariant(V_TX).as(), "TX pin (-1 default)", 1, -1, NUM_OUPUT_PINS); - interf->json_section_end(); // end of line - - interf->button(button_t::submit, A_SET_UART, T_DICT[lang][TD::D_Apply]); - interf->json_section_end(); // end of "uart" - - // counter opts - interf->spacer("Energy counter options"); - interf->json_section_begin(A_SET_PZOPTS); - interf->number(V_EOFFSET, espem->ds.getEnergyOffset(), "Energy counter offset"); - interf->button(button_t::submit, A_SET_PZOPTS, T_DICT[lang][TD::D_Apply]); - interf->json_section_end(); // end of "energy" - - - - interf->spacer("Metrics collector options"); - String _msg("Metrics pool capacity: "); - _msg += espem->ds.getMetricsSize(); - _msg += "/"; - _msg += espem->ds.getMetricsCap(); // current number of metrics samples - _msg += " samples"; - - interf->constant("mcap", _msg); - - // Button "Apply Metrics pool settings" - interf->button(button_t::submit, A_set_espem_pool, T_DICT[lang][TD::D_Apply]); - - // - // Define metrics collector state - // 0: Disabled, memory released - // 1: Running and storing metrics in RAM - // 2: Paused, collecting but not storing, memory reserved - // - interf->select(A_ECOLLECTORSTATE, (uint8_t)espem->get_collector_state(), "Metrics collector status", true); - interf->option(0, "Disabled"); - interf->option(1, "Running"); - interf->option(2, "Paused"); - interf->json_section_end(); // select - - interf->json_frame_flush(); // flush frame -*/ } /** * ESPEM data export page * */ -void ui_page_dataexport(Interface *interf, const JsonObject *data, const char* action){ +void ui_page_dataexport(Interface *interf, JsonObjectConst data, const char* action){ interf->json_frame_interface(); interf->json_section_uidata(); - interf->uidata_pick("espem.ui.export"); + interf->uidata_pick("espem.ui.pages.export"); interf->json_frame_flush(); } @@ -264,19 +224,19 @@ void ui_page_dataexport(Interface *interf, const JsonObject *data, const char* a /** * Apply espem options values */ -void set_sampler_opts(Interface *interf, const JsonObject *data, const char* action){ - if (!data) return; +void set_sampler_opts(Interface *interf, JsonObjectConst data, const char* action){ + if (!data.size()) return; // save sampling storage capacity values - SETPARAM(V_TS_T1_CNT); - SETPARAM(V_TS_T1_INT); - SETPARAM(V_TS_T2_CNT); - SETPARAM(V_TS_T2_INT); - SETPARAM(V_TS_T3_CNT); - SETPARAM(V_TS_T3_INT); + embui.getConfig()[V_TS_T1_CNT] = data[V_TS_T1_CNT]; + embui.getConfig()[V_TS_T1_INT] = data[V_TS_T1_INT]; + embui.getConfig()[V_TS_T2_CNT] = data[V_TS_T2_CNT]; + embui.getConfig()[V_TS_T2_INT] = data[V_TS_T2_INT]; + embui.getConfig()[V_TS_T3_CNT] = data[V_TS_T3_CNT]; + embui.getConfig()[V_TS_T3_INT] = data[V_TS_T3_INT]; espem->ds.reset(); // display main page - if (interf) ui_page_espem(interf, nullptr, NULL); + if (interf) ui_page_espem(interf, {}, NULL); } @@ -284,7 +244,7 @@ void set_sampler_opts(Interface *interf, const JsonObject *data, const char* act * обработка "живых" переключателей * */ -void set_directctrls(Interface *interf, const JsonObject *data, const char* action){ +void set_directctrls(Interface *interf, JsonObjectConst data, const char* action){ if (!data) return; std::string_view sv(action); @@ -292,55 +252,53 @@ void set_directctrls(Interface *interf, const JsonObject *data, const char* acti // ena/disable polling if (sv.compare(V_EPOLLENA) == 0){ - espem->set_uirate( (*data)[action] ? embui.paramVariant(V_UI_UPDRT) : 0); - LOG(printf, "ESPEM: UI refresh state: %d\n", (*data)[A_EPOLLENA].as() ); + espem->set_uirate( data[action] ? embui.getConfig()[V_UI_UPDRT] : 0); + LOGI(WebUI, printf, "UI refresh state: %d\n", data[A_EPOLLENA].as() ); return; } // UI update rate if (sv.compare(V_UI_UPDRT) == 0){ - espem->set_uirate((*data)[action]); - embui.var(V_UI_UPDRT, espem->get_uirate()); - LOG( printf, "ESPEM: Set UI update rate to: %d\n", espem->get_uirate() ); + espem->set_uirate(data[action]); + embui.getConfig()[V_UI_UPDRT] = espem->get_uirate(); + LOGI(WebUI, printf, "Set UI update rate to: %d\n", espem->get_uirate() ); return; } // Metrics collector run/pause if (sv.compare(V_ECOLLECTORSTATE) == 0){ - uint8_t new_state = (*data)[action]; + uint8_t new_state = data[action]; // reset TS Container if empty and we need to start it //if (espem->get_collector_state() == mcstate_t::MC_DISABLE && new_state >0) - // espem->ds.tsSet(embui.paramVariant(V_EPOOLSIZE), embui.paramVariant(V_SMPL_PERIOD)); + // espem->ds.tsSet(embui.getConfig()[V_EPOOLSIZE], embui.getConfig()[V_SMPL_PERIOD]); espem->set_collector_state((mcstate_t)new_state); - LOG(printf, "UI: Set TS Collector state to: %d\n", (int)espem->get_collector_state() ); + LOGD(WebUI, printf, "UI: Set TS Collector state to: %d\n", (int)espem->get_collector_state() ); return; } // Metrics graph - number of samples to draw in a small power chart if (sv.compare(C_scnt) == 0){ - embui.var(V_SMPLCNT, (*data)[action]); + embui.getConfig()[V_SMPLCNT] = data[action]; // send update command to AmCharts block if (interf){ - interf->json_frame("espem"); - interf->value(C_scnt, (*data)[action]); + interf->json_frame(C_espem); + interf->value(C_scnt, data[action]); interf->json_frame_flush(); } return; } + // select another tier on minichart if (sv.compare(C_tier) == 0){ // save new TS id - power_chart_id = (*data)[action]; + power_chart_id = data[action]; + LOGI(WebUI, printf, "Switch TS tier to:%d\n", power_chart_id ); if (!interf) return; - - // call js function to build power chart + // publish control values + ui_block_chart_ctrls_values(interf); + // call js function to re-build power chart ui_frame_mkchart(interf); - - // send update command to AmCharts block - interf->json_frame_interface(); - ui_block_chart_ctrls(interf); - interf->json_frame_flush(); return; } } @@ -348,27 +306,27 @@ void set_directctrls(Interface *interf, const JsonObject *data, const char* acti /** * Apply uart options values */ -void set_uart_opts(Interface *interf, const JsonObject *data, const char* action){ +void set_uart_opts(Interface *interf, JsonObjectConst data, const char* action){ if (!data) return; - uint8_t p = (*data)[V_UART].as(); + uint8_t p = data[V_UART].as(); if ( p <= SOC_UART_NUM ){ - SETPARAM(V_UART); + embui.getConfig()[V_UART] = data[V_UART]; } else return; - int r = (*data)[V_RX].as(); + int r = data[V_RX].as(); if (r <= NUM_OUPUT_PINS && r >0) { - SETPARAM(V_RX); + embui.getConfig()[V_RX] = data[V_RX]; } else return; - int t = (*data)[V_TX].as(); + int t = data[V_TX].as(); if (t <= NUM_OUPUT_PINS && r >0){ - SETPARAM(V_TX); + embui.getConfig()[V_TX] = data[V_TX]; } else return; - espem->begin(embui.paramVariant(V_UART), embui.paramVariant(V_RX), embui.paramVariant(V_TX)); + espem->begin(embui.getConfig()[V_UART], embui.getConfig()[V_RX], embui.getConfig()[V_TX]); // display main page - if (interf) ui_page_espem(interf, nullptr, NULL); + if (interf) ui_page_espem(interf, {}, NULL); } /** @@ -377,14 +335,14 @@ void set_uart_opts(Interface *interf, const JsonObject *data, const char* action * @param interf * @param data */ -void set_pzopts(Interface *interf, const JsonObject *data, const char* action){ +void set_pzopts(Interface *interf, JsonObjectConst data, const char* action){ if (!data) return; - SETPARAM(V_EOFFSET); - espem->ds.setEnergyOffset(embui.paramVariant(V_EOFFSET)); + embui.getConfig()[V_EOFFSET] = data[V_EOFFSET]; + espem->ds.setEnergyOffset(embui.getConfig()[V_EOFFSET]); // display main page - if (interf) ui_page_espem(interf, nullptr, NULL); + if (interf) ui_page_espem(interf, {}, NULL); } @@ -397,24 +355,7 @@ void set_pzopts(Interface *interf, const JsonObject *data, const char* action){ * */ void embui_actions_register(){ - LOG(println, "UI: Creating application vars"); - - /** - * регистрируем свои переменные - */ - embui.var_create(V_UI_UPDRT, DEFAULT_WS_UPD_RATE); // WebUI update rate - // Time Series values - embui.var_create(V_TS_T1_CNT, TS_T1_CNT); - embui.var_create(V_TS_T1_INT, TS_T1_INTERVAL); - embui.var_create(V_TS_T2_CNT, TS_T2_CNT); - embui.var_create(V_TS_T2_INT, TS_T2_INTERVAL); - embui.var_create(V_TS_T3_CNT, TS_T3_CNT); - embui.var_create(V_TS_T3_INT, TS_T3_INTERVAL); - embui.var_create(V_UART, 0x1); // default UART port UART_NUM_1 - embui.var_create(V_RX, -1); // RX pin (default) - embui.var_create(V_TX, -1); // TX pin (default) - embui.var_create(V_TX, -1); // TX pin (default) - embui.var_create(V_EOFFSET, 0); // Energy counter offset + LOGD(WebUI, println, "UI: Creating application vars"); /** * обработчики действий @@ -422,7 +363,7 @@ void embui_actions_register(){ // UI page callback handlers embui.action.set_mainpage_cb(ui_page_main); // index page callback - embui.action.set_settings_cb(user_settings_frame); // "settings" page options callback + embui.action.set_settings_cb(block_user_settings); // "settings" page options callback //embui.action.set_publish_cb(pubCallback); // Publish callback // вывод WebUI секций diff --git a/espem/interface.h b/espem/interface.h index 1a469bc..464f883 100644 --- a/espem/interface.h +++ b/espem/interface.h @@ -5,15 +5,15 @@ void embui_actions_register(); // Interface blocks void block_menu(Interface *interf); -void block_page_main(Interface *interf, const JsonObject *data, const char* action); -void block_page_espemset(Interface *interf, const JsonObject *data, const char* action); +void block_page_main(Interface *interf, JsonObjectConst data, const char* action); +void block_page_espemset(Interface *interf, JsonObjectConst data, const char* action); // ACTIONS -void action_demopage(Interface *interf, const JsonObject *data, const char* action); -void set_sampler_opts(Interface *interf, const JsonObject *data, const char* action); -void set_directctrls(Interface *interf, const JsonObject *data, const char* action); -void set_uart_opts(Interface *interf, const JsonObject *data, const char* action); -void set_pzopts(Interface *interf, const JsonObject *data, const char* action); +void action_demopage(Interface *interf, JsonObjectConst data, const char* action); +void set_sampler_opts(Interface *interf, JsonObjectConst data, const char* action); +void set_directctrls(Interface *interf, JsonObjectConst data, const char* action); +void set_uart_opts(Interface *interf, JsonObjectConst data, const char* action); +void set_pzopts(Interface *interf, JsonObjectConst data, const char* action); // Callbacks void pubCallback(Interface *interf); diff --git a/espem/log.h b/espem/log.h new file mode 100644 index 0000000..1649e48 --- /dev/null +++ b/espem/log.h @@ -0,0 +1,82 @@ +/* +LOG macro will enable/disable logs to serial depending on ESPEM_DEBUG build-time flag +*/ +#pragma once + +#ifndef ESPEM_DEBUG_PORT +#define ESPEM_DEBUG_PORT Serial +#endif + +#ifndef ESPEM_DEBUG_LEVEL +#define ESPEM_DEBUG_LEVEL 2 +#endif + +// undef possible LOG macros +#ifdef LOG + #undef LOG +#endif +#ifdef LOGV + #undef LOGV +#endif +#ifdef LOGD + #undef LOGD +#endif +#ifdef LOGI + #undef LOGI +#endif +#ifdef LOGW + #undef LOGW +#endif +#ifdef LOGE + #undef LOGE +#endif + +static constexpr const char* S_V = "V: "; +static constexpr const char* S_D = "D: "; +static constexpr const char* S_I = "I: "; +static constexpr const char* S_W = "W: "; +static constexpr const char* S_E = "E: "; + +#if defined(ESPEM_DEBUG_LEVEL) && ESPEM_DEBUG_LEVEL == 5 + #define LOGV(tag, func, ...) ESPEM_DEBUG_PORT.print(S_V); ESPEM_DEBUG_PORT.print(tag); ESPEM_DEBUG_PORT.print((char)0x9); ESPEM_DEBUG_PORT.func(__VA_ARGS__) +#else + #define LOGV(...) +#endif + +#if defined(ESPEM_DEBUG_LEVEL) && ESPEM_DEBUG_LEVEL > 3 + #define LOGD(tag, func, ...) ESPEM_DEBUG_PORT.print(S_D); ESPEM_DEBUG_PORT.print(tag); ESPEM_DEBUG_PORT.print((char)0x9); ESPEM_DEBUG_PORT.func(__VA_ARGS__) +#else + #define LOGD(...) +#endif + +#if defined(ESPEM_DEBUG_LEVEL) && ESPEM_DEBUG_LEVEL > 2 + #define LOGI(tag, func, ...) ESPEM_DEBUG_PORT.print(S_I); ESPEM_DEBUG_PORT.print(tag); ESPEM_DEBUG_PORT.print((char)0x9); ESPEM_DEBUG_PORT.func(__VA_ARGS__) + // compat macro + #define LOG(func, ...) ESPEM_DEBUG_PORT.func(__VA_ARGS__) +#else + #define LOGI(...) + // compat macro + #define LOG(...) +#endif + +#if defined(ESPEM_DEBUG_LEVEL) && ESPEM_DEBUG_LEVEL > 1 + #define LOGW(tag, func, ...) ESPEM_DEBUG_PORT.print(S_W); ESPEM_DEBUG_PORT.print(tag); ESPEM_DEBUG_PORT.print((char)0x9); ESPEM_DEBUG_PORT.func(__VA_ARGS__) +#else + #define LOGW(...) +#endif + +#if defined(ESPEM_DEBUG_LEVEL) && ESPEM_DEBUG_LEVEL > 0 + #define LOGE(tag, func, ...) ESPEM_DEBUG_PORT.print(S_E); ESPEM_DEBUG_PORT.print(tag); ESPEM_DEBUG_PORT.print((char)0x9); ESPEM_DEBUG_PORT.func(__VA_ARGS__) +#else + #define LOGE(...) +#endif + + +// LOG tags +static constexpr const char* T_Effect = "Effect"; +static constexpr const char* T_EffCfg = "EffCfg"; +static constexpr const char* T_EffWrkr = "EffWrkr"; +static constexpr const char* T_Fade = "Fade"; +static constexpr const char* T_Module = "Module"; +static constexpr const char* T_WebUI = "WebUI"; +static constexpr const char* T_ModMGR = "ModMGR"; diff --git a/espem/main.cpp b/espem/main.cpp index bef7c1d..368154b 100644 --- a/espem/main.cpp +++ b/espem/main.cpp @@ -2,7 +2,7 @@ * A code for ESP32 based boards to interface with PeaceFair PZEM PowerMeters * It can poll/collect PowerMeter data and provide it for futher processing in text/json format * - * (c) Emil Muratov 2017-2022 + * (c) Emil Muratov 2017-2024 * */ @@ -10,60 +10,84 @@ #include "main.h" #include "espem.h" -#include +#include "EmbUI.h" #include "interface.h" +#include "log.h" -extern "C" int clock_gettime(clockid_t unused, struct timespec *tp); +#define BAUD_RATE 115200 // serial debug port baud rate +#define HTTP_VER_BUFSIZE 256 - -// PROGMEM strings // sprintf template for json version data -static const char PGverjson[] = "{\"ChipID\":\"%s\",\"Flash\":%u,\"SDK\":\"%s\",\"firmware\":\"" FW_NAME "\",\"version\":\"" FW_VERSION_STRING "\",\"git\":\"%s\",\"CPUMHz\":%u,\"RAM Heap size\":%u,\"RAM Heap free\":%u,\"PSRAM size\":%u,\"PSRAM free\":%u,\"Uptime\":%u}"; +static constexpr const char* PGverjson = "{\"ChipID\":\"%s\",\"Flash\":%u,\"SDK\":\"%s\",\"firmware\":\"" FW_NAME "\",\"version\":\"" FW_VERSION_STRING "\",\"git\":\"%s\",\"CPUMHz\":%u,\"RAM Heap size\":%u,\"RAM Heap free\":%u,\"PSRAM size\":%u,\"PSRAM free\":%u,\"Uptime\":%u}"; // Our instance of espem Espem *espem = nullptr; +// forward declaration +void wver(AsyncWebServerRequest *request); + +// load configuration for espem and start it +void setup_espem(); + // ---- // MAIN Setup void setup() { -#ifdef ESPEM_DEBUG - ESPEM_DEBUG.begin(BAUD_RATE); // start hw serial for debugging +#ifdef ESPEM_DEBUG_PORT + ESPEM_DEBUG_PORT.begin(BAUD_RATE); // start hw serial for debugging #endif - LOG(println, F("Starting EspEM...")); + LOGI(C_EspEM, println, "Starting EspEM..."); // Start framework, load config and connect WiFi embui.begin(); + + // start EspEM object + setup_espem(); + + // attach EmbUI callback actions embui_actions_register(); - // create and run ESPEM object - espem = new Espem(); + // status call + embui.server.on("/fw", HTTP_GET, [](AsyncWebServerRequest *request){ wver(request); }); + + // adjust EmbUI's publish rate + embui.setPubInterval(WEBUI_PUBLISH_INTERVAL); +} - if (espem && espem->begin( embui.paramVariant(V_UART), - embui.paramVariant(V_RX), - embui.paramVariant(V_TX)) - ) - { - espem->ds.setEnergyOffset(embui.paramVariant(V_EOFFSET)); +// MAIN loop +void loop() { + embui.handle(); +} + +// load configuration for espem and start it +void setup_espem(){ + if (!espem){ + // create and run ESPEM object + espem = new Espem(); + } + + if (!espem) return; + + if (espem->begin( + embui.getConfig()[V_UART] | UART_NUM_1, // by default UART_NUM_1 uses SECOND uart port on ESP32, not the one that outputs console serial + embui.getConfig()[V_RX] | -1, + embui.getConfig()[V_TX] | -1) + ){ + LOGI(C_EspEM, printf, "attaching to UART:%d\n",embui.getConfig()[V_UART] | UART_NUM_1 ); + + espem->ds.setEnergyOffset(embui.getConfig()[V_EOFFSET]); + LOGI(C_EspEM, printf, "Configured energy offset is:%d\n", embui.getConfig()[V_EOFFSET] | 0 ); // postpone TimeSeries setup until NTP aquires valid time TimeProcessor::getInstance().attach_callback([](){ + LOGI(C_EspEM, println, "Aquired time from NTP, running TimeSeries collector..." ); espem->set_collector_state(mcstate_t::MC_RUN); // we only need that setup once TimeProcessor::getInstance().dettach_callback(); }); } - - embui.server.on("/fw", HTTP_GET, [](AsyncWebServerRequest *request){ wver(request); }); - - embui.setPubInterval(WEBUI_PUBLISH_INTERVAL); -} - -// MAIN loop -void loop() { - embui.handle(); } // send HTTP responce, json with controller/fw versions and status info @@ -72,7 +96,7 @@ void wver(AsyncWebServerRequest *request) { timespec tp; clock_gettime(0, &tp); - snprintf_P(buff, sizeof(buff), PGverjson, + snprintf(buff, sizeof(buff), PGverjson, ESP.getChipModel(), ESP.getFlashChipSize(), ESP.getSdkVersion(), @@ -87,6 +111,5 @@ void wver(AsyncWebServerRequest *request) { (uint32_t)tp.tv_sec); - request->send(200, FPSTR(PGmimejson), buff ); + request->send(200, asyncsrv::T_application_json, buff ); } -// \ No newline at end of file diff --git a/espem/main.h b/espem/main.h index 49227e0..64ee145 100644 --- a/espem/main.h +++ b/espem/main.h @@ -2,7 +2,7 @@ * A code for ESP32 based boards to interface with PeaceFair PZEM PowerMeters * It can poll/collect PowerMeter data and provide it for futher processing in text/json format * - * (c) Emil Muratov 2018 - 2023 + * (c) Emil Muratov 2018 - 2024 * */ @@ -24,24 +24,7 @@ #define ESPEM_UI_VERSION 2 -#define BAUD_RATE 115200 // serial debug port baud rate -#define HTTP_VER_BUFSIZE 256 - #define WEBUI_PUBLISH_INTERVAL 20 // Sketch configuration -#include "globals.h" // EmbUI macro's for LOG -#include "config.h" #include "uistrings.h" // non-localized text-strings -#include - -// EMBUI -void create_parameters(); // декларируем для переопределения weak метода из фреймворка для WebUI -void sync_parameters(); - -// WiFi connection callback -void onSTAGotIP(); -// Manage network disconnection -void onSTADisconnected(); - -void wver(AsyncWebServerRequest *request); \ No newline at end of file diff --git a/espem/uistrings.h b/espem/uistrings.h index 1a5ef73..e7538e9 100644 --- a/espem/uistrings.h +++ b/espem/uistrings.h @@ -3,57 +3,60 @@ // Set of flash-strings that might be reused multiple times within the code // General -static constexpr const char C_espem[] = "espem"; -static constexpr const char C_espem_ui[] = "espem.ui"; -static constexpr const char C_mkchart[] = "mkchart"; -static constexpr const char C_mkgauge[] = "mkgauge"; -static constexpr const char C_gsmini[] = "gsmini"; -static constexpr const char C_mqtt_pzem_jmetrics[] = "pub/pzem/jmetrics"; -static constexpr const char C_scnt[] = "scnt"; // samle counter -static constexpr const char C_tier[] = "tier"; -static constexpr const char C_lchart[] = "lchart"; +static constexpr const char* C_EspEM = "EspEM"; +static constexpr const char* C_espem = "espem"; +static constexpr const char* C_espem_ui = "espem.ui"; +static constexpr const char* C_mkchart = "mkchart"; +static constexpr const char* C_mkgauge = "mkgauge"; +static constexpr const char* C_minichart = "minichart"; +static constexpr const char* C_smpchart = "smpchart"; +static constexpr const char* C_mqtt_pzem_jmetrics = "pub/pzem/jmetrics"; +static constexpr const char* C_sample = "sample"; +static constexpr const char* C_scnt = "scnt"; // samle counter +static constexpr const char* C_tier = "tier"; +static constexpr const char* C_lchart = "lchart"; ////////////////////// // Configuration variables names - V_ prefix for 'Variable' -static constexpr const char V_TS_T1_CNT[] = "t1cnt"; // default Tier 1 TimeSeries count -static constexpr const char V_TS_T1_INT[] = "t1int"; // default Tier 1 TimeSeries interval -static constexpr const char V_TS_T2_CNT[] = "t2cnt"; // default Tier 2 TimeSeries count -static constexpr const char V_TS_T2_INT[] = "t2int"; // default Tier 2 TimeSeries interval -static constexpr const char V_TS_T3_CNT[] = "t3cnt"; // default Tier 3 TimeSeries count -static constexpr const char V_TS_T3_INT[] = "t3int"; // default Tier 3 TimeSeries interval -static constexpr const char V_RX[] = "rx"; // rx pin -static constexpr const char V_TX[] = "tx"; // tx ping -static constexpr const char V_UART[] = "uart"; // uart interface -static constexpr const char V_EOFFSET[] = "eoffset"; // energy offset +static constexpr const char* V_TS_T1_CNT = "t1cnt"; // default Tier 1 TimeSeries count +static constexpr const char* V_TS_T1_INT = "t1int"; // default Tier 1 TimeSeries interval +static constexpr const char* V_TS_T2_CNT = "t2cnt"; // default Tier 2 TimeSeries count +static constexpr const char* V_TS_T2_INT = "t2int"; // default Tier 2 TimeSeries interval +static constexpr const char* V_TS_T3_CNT = "t3cnt"; // default Tier 3 TimeSeries count +static constexpr const char* V_TS_T3_INT = "t3int"; // default Tier 3 TimeSeries interval +static constexpr const char* V_RX = "rx"; // rx pin +static constexpr const char* V_TX = "tx"; // tx ping +static constexpr const char* V_UART = "uart"; // uart interface +static constexpr const char* V_EOFFSET = "eoffset"; // energy offset // directly changed vars, must match actions with prefixed "dctl_" -static constexpr const char V_EPOLLENA[] = "poll"; // Enable/disable poller -static constexpr const char V_EPFFIX[] = "pffix"; // PowerFactor value correction -static constexpr const char V_UI_UPDRT[] = "updaterate"; // UI update rate -static constexpr const char V_ECOLLECTORSTATE[] = "collector"; // Metrics collector run/pause -static constexpr const char V_SMPLCNT[] = "smplcnt"; // Metrics graph - number of samples to draw in a small power chart +static constexpr const char* V_EPOLLENA = "poll"; // Enable/disable poller +static constexpr const char* V_EPFFIX = "pffix"; // PowerFactor value correction +static constexpr const char* V_UI_UPDRT = "updaterate"; // UI update rate +static constexpr const char* V_ECOLLECTORSTATE = "collector"; // Metrics collector run/pause +static constexpr const char* V_SMPLCNT = "smplcnt"; // Metrics graph - number of samples to draw in a small power chart // UI blocks - B_ prefix for 'web Block' -static constexpr const char A_ui_page_espem[] = "ui_page_espem"; -static constexpr const char A_ui_page_espem_setup[] = "ui_page_espem_setup"; -static constexpr const char A_ui_page_dataexport[] = "ui_page_dataexport"; +static constexpr const char* A_ui_page_espem = "ui_page_espem"; +static constexpr const char* A_ui_page_espem_setup = "ui_page_espem_setup"; +static constexpr const char* A_ui_page_dataexport = "ui_page_dataexport"; // direct control elements -static constexpr const char A_DIRECT_CTL[] = "dctl_*"; // checkboxes/controls that should be processed onChange +static constexpr const char* A_DIRECT_CTL = "dctl_*"; // checkboxes/controls that should be processed onChange // UI handlers - A_ prefix for 'Action' -static constexpr const char A_set_espem_pool[] = "set_espem_pool"; // ESPEM settings update -static constexpr const char A_SET_UART[] = "set_uart"; -static constexpr const char A_SET_PZOPTS[] = "set_nrgoffset"; -static constexpr const char A_SET_MCOLLECTOR[] = "set_mcollector"; // apply metrics collector settings +static constexpr const char* A_set_espem_pool = "set_espem_pool"; // ESPEM settings update +static constexpr const char* A_SET_UART = "set_uart"; +static constexpr const char* A_SET_PZOPTS = "set_nrgoffset"; +static constexpr const char* A_SET_MCOLLECTOR = "set_mcollector"; // apply metrics collector settings // onChange controls actions -static constexpr const char A_EPOLLENA[] = "dctl_poll"; // Enable/disable poller -static constexpr const char A_EPFFIX[] = "dctl_pffix"; // PowerFactor value correction -static constexpr const char A_UI_UPDRT[] = "dctl_updaterate"; // UI update rate -static constexpr const char A_ECOLLECTORSTATE[] = "dctl_collector"; // Metrics collector run/pause -static constexpr const char A_SMPLCNT[] = "dctl_scnt"; // Metrics graph - number of samples to draw in a small power chart -static constexpr const char A_TS_TIER[] = "dctl_tier"; // drop-down selector for power chart TS id +static constexpr const char* A_EPOLLENA = "dctl_poll"; // Enable/disable poller +static constexpr const char* A_EPFFIX = "dctl_pffix"; // PowerFactor value correction +static constexpr const char* A_UI_UPDRT = "dctl_updaterate"; // UI update rate +static constexpr const char* A_ECOLLECTORSTATE = "dctl_collector"; // Metrics collector run/pause +static constexpr const char* A_SMPLCNT = "dctl_scnt"; // Metrics graph - number of samples to draw in a small power chart +static constexpr const char* A_TS_TIER = "dctl_tier"; // drop-down selector for power chart TS id // other constants diff --git a/extra/0001-LDF-refresh-lib-dependency-after-recursive-search.patch b/extra/0001-LDF-refresh-lib-dependency-after-recursive-search.patch new file mode 100644 index 0000000..ca83002 --- /dev/null +++ b/extra/0001-LDF-refresh-lib-dependency-after-recursive-search.patch @@ -0,0 +1,29 @@ +From 7d12a010a2a71428e6945c8c9c2d05073df379a2 Mon Sep 17 00:00:00 2001 +From: Emil Muratov +Date: Fri, 21 Jun 2024 17:01:38 +0900 +Subject: [PATCH] LDF: refresh lib dependency after recursive search + +LDF might mistakenly remove recursive dependency libs from a graph +usually platform bundled ones + +Closes #4940 +--- + platformio/builder/tools/piolib.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/platformio/builder/tools/piolib.py b/platformio/builder/tools/piolib.py +index ca9c9f1..36b72d2 100644 +--- a/platformio/builder/tools/piolib.py ++++ b/platformio/builder/tools/piolib.py +@@ -1159,6 +1159,8 @@ def ConfigureProjectLibBuilder(env): + for lb in lib_builders: + if lb in found_lbs: + lb.search_deps_recursive(lb.get_search_files()) ++ # refill found libs after recursive search ++ found_lbs = [lb for lb in lib_builders if lb.is_dependent] + for lb in lib_builders: + for deplb in lb.depbuilders[:]: + if deplb not in found_lbs: +-- +2.34.1 + diff --git a/platformio.ini b/platformio.ini index 4d7624b..e677ca3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,8 +9,8 @@ extra_configs = board_build.filesystem = littlefs framework = arduino build_flags = - -DFZ_WITH_ASYNCSRV - -DNO_GLOBAL_UPDATE + -DFZ_WITH_ASYNCSRV -DFZ_NOHTTPCLIENT -DNO_GLOBAL_UPDATE + -DEMBUI_IDPREFIX='"EspEM"' ; -DCOUNTRY="ru" build_src_flags = !python flags.py @@ -18,7 +18,9 @@ build_src_flags = src_build_unflags = -std=gnu++11 lib_deps = - https://github.com/vortigont/EmbUI.git#v3.1 + vortigont/pzem-edl @ ~1.2 + https://github.com/vortigont/EmbUI.git#idf5.3 +; vortigont/EmbUI @ ~4.0.2 lib_ignore = ESPAsyncTCP monitor_speed = 115200 @@ -28,9 +30,10 @@ monitor_speed = 115200 [debug] espem_serial = -DESPEM_DEBUG=Serial + -DESPEM_DEBUG_LEVEL=4 app_serial = ${debug.espem_serial} - -DEMBUI_DEBUG + -DEMBUI_DEBUG_LEVEL=3 -DEMBUI_DEBUG_PORT=Serial all_serial = ${debug.app_serial} @@ -42,7 +45,7 @@ espem_serial1 = -DESPEM_DEBUG=Serial1 app_serial1 = ${debug.espem_serial1} - -DEMBUI_DEBUG + -DEMBUI_DEBUG_LEVEL=3 -DEMBUI_DEBUG_PORT=Serial1 all_serial1 = ${debug.espem_serial1} @@ -50,33 +53,24 @@ all_serial1 = [esp32_base] extends = common -platform = espressif32 @ 6.9.0 +; Tasmota's platform, 2024.11.30 Tasmota Arduino Core 3.1.0.241030 based on IDF 5.3.1+ +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.11.30/platform-espressif32.zip +; Tasmota's platform, based on Arduino Core v3.0.4 +;platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.08.11/platform-espressif32.zip +;platform = espressif32 @ 6.9.0 board = wemos_d1_mini32 -;upload_speed = 460800 monitor_filters = esp32_exception_decoder ;build_flags = -lib_ignore = - ESPAsyncTCP - LITTLEFS - LittleFS_esp32 ; ===== Build ENVs ====== [env] extends = common -;build_flags = -; -DCOUNTRY="ru" // Country double-letter code, used for NTP pool selection -; -DNO_GLOBAL_SERIAL -; -DNO_GLOBAL_SERIAL1 ; ESP32 with PZEM EDL [env:espem] extends = esp32_base -lib_deps = - ${esp32_base.lib_deps} - vortigont/pzem-edl @ ~1.2 -; https://github.com/vortigont/pzem-edl ;build_flags = ; ${esp32_base.build_flags} ; -DCOUNTRY="ru" @@ -88,10 +82,6 @@ lib_deps = ; ESP32 with PZEM EDL, debug enabled [env:espem_debug] extends = esp32_base -lib_deps = - ${esp32_base.lib_deps} - https://github.com/vortigont/pzem-edl -; vortigont/pzem-edl @ ~1.0.0 build_flags = ${esp32_base.build_flags} ${debug.app_serial} @@ -100,9 +90,6 @@ build_flags = ; build pzem emulator [env:espem_dummy] extends = esp32_base -lib_deps = - ${esp32_base.lib_deps} - https://github.com/vortigont/pzem-edl build_flags = ${esp32_base.build_flags} -DESPEM_DUMMY diff --git a/resources/html/index.html b/resources/html/index.html index 59b1cb3..3537474 100644 --- a/resources/html/index.html +++ b/resources/html/index.html @@ -104,13 +104,16 @@ {{/if}} {{#if type == "checkbox"}}
- +
{{/if}} {{#if html == "const"}}
{{label}}{{#if2 value}}{{/if2}}
{{/if}} + {{#if html == "constbtn"}} + + {{/if}} {{#if html == "comment"}}
{{label}}
{{/if}} @@ -132,7 +135,11 @@ param.* - reserved --> {{#if2 label}}{{label}}{{/if2}} -
{{value}}
+
{{value}}
+ {{/if}} + {{#if html == "div" && type == "chart"}} + {{#if2 label}}{{label}}{{/if2}} +
{{/if}} {{#if html == "div" && type == "pbar"}} @@ -191,10 +198,6 @@

{{label}}

- diff --git a/resources/html/js/espem.js b/resources/html/js/espem.js index 71d3e8a..ca05dd9 100644 --- a/resources/html/js/espem.js +++ b/resources/html/js/espem.js @@ -242,7 +242,8 @@ function mkgauge(id, param){ } } -function mkchart(obj){ +function mkcharts(obj){ + console.log('mkcharts:', obj); let id = obj.block[0].id; minichart.tier = obj.block[0].tier; minichart.scnt = obj.block[0].scnt; @@ -349,3 +350,18 @@ function mkchart(obj){ console.log("created gsmini"); } + +// register user functions +customFuncs["mkchart"] = mkcharts; +customFuncs["mkgauge"] = mkgauge; + + +// load EspEM's App UIData +window.addEventListener("load", async function(ev){ + let response = await fetch("/js/espem.ui.json", {method: 'GET'}); + if (response.ok){ + response = await response.json(); + uiblocks['espem'] = {"ui": response}; + } + +}.bind(window)) diff --git a/resources/html/js/espem.ui.json b/resources/html/js/espem.ui.json index f071e90..d6f0451 100644 --- a/resources/html/js/espem.ui.json +++ b/resources/html/js/espem.ui.json @@ -2,265 +2,344 @@ "type": "interface", "version": 2, "descr": "EspEM UI objects", - "settings": { - "cfg": { - "section": "ui_page_empem_setup", - "label": "ESPEM Setup", + "pages":{ + "main":{ + "section": "ui_page_espem", + "label": "EspEnergy Meter", "main": true, "block": [ { - "section": "set_uart", - "label": "UART GPIO Setup", - "hidden": true, - "block": [ + "section": "live_update_ctrls", + "line": true, + "block":[ { - "section": "uart_gpio", - "line": true, - "block": [ - { - "id": "uart", - "html": "input", - "value": 1, - "type": "number", - "label": "Uart port", - "max": 3, - "step": 1 - }, - { - "id": "rx", - "html": "input", - "value": -1, - "type": "number", - "label": "RX pin (-1 to disable)", - "min": -1, - "max": 46, - "step": 1 - }, - { - "id": "tx", - "html": "input", - "value": -1, - "type": "number", - "label": "TX pin (-1 to disable)", - "min": -1, - "max": 46, - "step": 1 - } - ] + "id":"dctl_poll", + "html":"input", + "label":"Live-update", + "type":"checkbox", + "onChange": true }, { - "id": "set_uart", - "html": "button", - "type": 1, - "label": "Apply" + "id": "dctl_updaterate", + "html": "input", + "type": "range", + "min": 0, + "max": 30, + "step": 1, + "onChange": true, + "label": "Update rate, sec" } ] }, { - "section": "set_nrgoffset", - "label": "PZEM Options", - "hidden": true, - "block": [ + "section": "displays", + "line": true, + "block":[ { - "html": "spacer", - "label": "Energy counter options" + "id":"pwr", + "html":"div", + "type":"html", + "label":"Power", + "class": "display pwr" }, { - "id": "eoffset", - "html": "input", - "value": 0, - "type": "number", - "label": "Energy counter offset (Wh)" + "id":"cur", + "html":"div", + "type":"html", + "label":"Current", + "class": "display cur" }, { - "id": "set_nrgoffset", - "html": "button", - "type": 1, - "label": "Apply" + "id":"enrg", + "html":"div", + "type":"html", + "label":"Energy", + "class": "display enrg" } ] }, { - "section": "set_mcollector", - "label": "Time Series Collector", - "hidden": true, - "block": [ - { - "html": "spacer", - "label": "Pool capacity setup" - }, - { - "id": "dctl_collector", - "html": "select", - "label": "Metrics collector state", - "onChange": true, - "section": "options", - "block": [ - { "value": 0, "label": "Disabled" }, - { "value": 1, "label": "Running" }, - { "value": 2, "label": "Paused" } - ] - }, - { - "section": "t1cmt", - "line": true, - "block": [ - { - "html": "comment", - "label": "Tier 1 series" - }, - { - "id": "t1mem", - "html": "const", - "label": "Memory: -/-" - } - ] - }, - { - "section": "t1opts", - "line": true, - "block": [ - { - "id": "t1cnt", - "html": "input", - "type": "number", - "label": "Num of samples", - "min": 100, - "step": 100 - }, - { - "id": "t1int", - "html": "input", - "value": 1, - "type": "number", - "label": "interval (sec.)", - "min": 1, - "step": 1 - } - ] - }, - { - "section": "t2cmt", - "line": true, - "block": [ - { - "html": "comment", - "label": "Tier 2 series" - }, - { - "id": "t2mem", - "html": "const", - "label": "Memory: -/-" - } - ] - }, + "section": "gauges", + "line": true, + "block":[ { - "section": "t2opts", - "line": true, - "block": [ - { - "id": "t2cnt", - "html": "input", - "type": "number", - "label": "Num of samples", - "min": 100, - "step": 100 - }, - { - "id": "t2int", - "html": "input", - "type": "number", - "label": "interval (sec.)", - "min": 5, - "step": 5 - } - ] + "id":"gaugeV", + "html":"div", + "type":"js", + "label":"Voltage", + "value":"mkgauge", + "class":"graphwide" }, { - "section": "t3cmt", - "line": true, - "block": [ - { - "html": "comment", - "label": "Tier 3 series" - }, - { - "id": "t3mem", - "html": "const", - "label": "Memory: -/-" - } - ] - }, - { - "section": "t3opts", - "line": true, - "block": [ - { - "id": "t3cnt", - "html": "input", - "type": "number", - "label": "Num of samples", - "min": 100, - "step": 100 - }, - { - "id": "t3int", - "html": "input", - "type": "number", - "label": "interval (sec.)", - "min": 60, - "step": 60 - } - ] - }, - { - "id": "set_mcollector", - "html": "button", - "type": 1, - "label": "Apply" + "id":"gaugePF", + "html":"div", + "type":"js", + "label":"Power Factor", + "value":"mkgauge", + "class":"graphwide" } ] + } + ] + }, + "settings":{ + "section": "ui_page_espem_setup", + "label": "ESPEM Setup", + "main": true, + "block": [ + { + "section": "set_uart", + "label": "UART GPIO Setup", + "hidden": true, + "block": [ + { + "section": "uart_gpio", + "line": true, + "block": [ + { + "id": "uart", + "html": "input", + "value": 1, + "type": "number", + "label": "Uart port", + "max": 3, + "step": 1 + }, + { + "id": "rx", + "html": "input", + "value": -1, + "type": "number", + "label": "RX pin (-1 to disable)", + "min": -1, + "max": 46, + "step": 1 + }, + { + "id": "tx", + "html": "input", + "value": -1, + "type": "number", + "label": "TX pin (-1 to disable)", + "min": -1, + "max": 46, + "step": 1 + } + ] + }, + { + "id": "set_uart", + "html": "button", + "type": 1, + "label": "Apply" + } + ] + }, + { + "section": "set_nrgoffset", + "label": "PZEM Options", + "hidden": true, + "block": [ + { + "html": "spacer", + "label": "Energy counter options" + }, + { + "id": "eoffset", + "html": "input", + "value": 0, + "type": "number", + "label": "Energy counter offset (Wh)" + }, + { + "id": "set_nrgoffset", + "html": "button", + "type": 1, + "label": "Apply" + } + ] + }, + { + "section": "set_mcollector", + "label": "Time Series Collector", + "hidden": true, + "block": [ + { + "html": "spacer", + "label": "Pool capacity setup" + }, + { + "id": "dctl_collector", + "html": "select", + "label": "Metrics collector state", + "onChange": true, + "section": "options", + "block": [ + { "value": 0, "label": "Disabled" }, + { "value": 1, "label": "Running" }, + { "value": 2, "label": "Paused" } + ] + }, + { + "section": "t1cmt", + "line": true, + "block": [ + { + "html": "comment", + "label": "Tier 1 series" + }, + { + "id": "t1mem", + "html": "const", + "label": "Memory: -/-" + } + ] + }, + { + "section": "t1opts", + "line": true, + "block": [ + { + "id": "t1cnt", + "html": "input", + "type": "number", + "label": "Num of samples", + "min": 100, + "step": 100 + }, + { + "id": "t1int", + "html": "input", + "value": 1, + "type": "number", + "label": "interval (sec.)", + "min": 1, + "step": 1 + } + ] + }, + { + "section": "t2cmt", + "line": true, + "block": [ + { + "html": "comment", + "label": "Tier 2 series" + }, + { + "id": "t2mem", + "html": "const", + "label": "Memory: -/-" + } + ] + }, + { + "section": "t2opts", + "line": true, + "block": [ + { + "id": "t2cnt", + "html": "input", + "type": "number", + "label": "Num of samples", + "min": 100, + "step": 100 + }, + { + "id": "t2int", + "html": "input", + "type": "number", + "label": "interval (sec.)", + "min": 5, + "step": 5 + } + ] + }, + { + "section": "t3cmt", + "line": true, + "block": [ + { + "html": "comment", + "label": "Tier 3 series" + }, + { + "id": "t3mem", + "html": "const", + "label": "Memory: -/-" + } + ] + }, + { + "section": "t3opts", + "line": true, + "block": [ + { + "id": "t3cnt", + "html": "input", + "type": "number", + "label": "Num of samples", + "min": 100, + "step": 100 + }, + { + "id": "t3int", + "html": "input", + "type": "number", + "label": "interval (sec.)", + "min": 60, + "step": 60 + } + ] + }, + { + "id": "set_mcollector", + "html": "button", + "type": 1, + "label": "Apply" + } + ] + }, + { + "id": "ui_page_settings", + "html": "button", + "type": 1, + "label": "Exit", + "color": "gray" + } + ] + }, + "export":{ + "section": "ui_page_espem_setup", + "label": "ESPEM Data Export", + "main": true, + "block": [ + { + "html": "comment", + "label": "Sampling data could be exported in json format.
Generic URI to get the data: http://[espem]/samples.json?tsid=X&scnt=YY
Where X is TS id (1-3)
YY - num of samples to receive" + }, + { + "html": "button", + "type": 3, + "label": "Download TimeSeries #1", + "color": "green", + "value": "/samples.json?tsid=1" + }, + { + "html": "button", + "type": 3, + "label": "Download TimeSeries #2", + "color": "green", + "value": "/samples.json?tsid=2" }, { - "id": "ui_page_settings", "html": "button", - "type": 1, - "label": "Exit", - "color": "gray" + "type": 3, + "label": "Download TimeSeries #3", + "color": "green", + "value": "/samples.json?tsid=3" } - ] - } - }, - "export":{ - "section": "ui_page_empem_setup", - "label": "ESPEM Data Export", - "main": true, - "block": [ - { - "html": "comment", - "label": "Sampling data could be exported in json format.
Generic URI to get the data: http://[espem]/samples.json?tsid=X&scnt=YY
Where X is TS id (1-3)
YY - num of samples to receive" - }, - { - "html": "button", - "type": 3, - "label": "Download TimeSeries #1", - "color": "green", - "value": "/samples.json?tsid=1" - }, - { - "html": "button", - "type": 3, - "label": "Download TimeSeries #2", - "color": "green", - "value": "/samples.json?tsid=2" - }, - { - "html": "button", - "type": 3, - "label": "Download TimeSeries #3", - "color": "green", - "value": "/samples.json?tsid=3" - } - ] + ] + } } } \ No newline at end of file