diff --git a/README.md b/README.md
index fc9c02d..a86573b 100644
--- a/README.md
+++ b/README.md
@@ -8,37 +8,97 @@ for [Peacefair](https://peacefair.aliexpress.com/store/1773456/) PZEM-004Tv30 Po
## Features
- WebUI via self-hosted HTTP/WebSocket server
- Real-time gauges and power charts
-- metrics collector in controller's memory pool
+- MQTT publishing metrics data in json format
+- 3 level Tiered TimeSeries metrics collector in controller's memory pool
- data/metrics export via json
- compressed OTA updating via [esp32-flashz](https://github.com/vortigont/esp32-flashz) lib
- espem could be build to run via DummyPZEM emulator for prototyping and firmware testing without connecting to a real PZEM device
+## ESPEM WebUI Dashboard
-## Legacy v2.x version
-An older ESPEM version 2 was based on 3rd party lib. It's code still available under [2.x branch](https://github.com/vortigont/espem/tree/v2).
-ESPEM Ver 3 switched to it's own library [pzem-edl](https://github.com/vortigont/pzem-edl). I wrote this lib to overcome limitations of the classic [olehs](https://github.com/olehs/PZEM004T)'s and [mandulaj](https://github.com/mandulaj/PZEM-004T-v30)'s libs. Being versatile those libs provided only basic functions talking to PZEM's using Arduino's blocking IO via serial port. New lib uses event-driven approach and provides extendable design API for multiple PZEM communication over single port.
+
-### Feature comparison
-| |[Version 2.x](https://github.com/vortigont/espem/tree/v2) |Version 3.0|
-|----------------|--------------|---------------|
-|ESP8266 |YES |NO |
-|ESP32 |YES |YES |
-|PZEM004v30 |YES |YES |
-|old PZEMv30 |YES |NO (planned) |
-|PZEM017 (DC version) |NO|NO (planned)|
-|3 phase option |NO|YES (in progress)|
-|Time Series Charts |YES (Basic)|YES (extendable)|
-|TS in PSRAM |NO|YES|
+## MQTT
+MQTT server publishing and connection could be configured via "Settings" - "MQTT".
+It will publish pzem metrics to topic `~/pub/pzem/jmetrics` on each update cycle. Where `~` is topic prefix, default is `EmbUI/[DeviceID]/`.
+Message structure is a dictionary with PZEM metrics in an integer format in a way PZEM sends it via MODBUS.
+`[{"stale":false,"age":999,"U":2180,"I":503,"P":778,"W":560,"Pf":71,"freq":505}]`
+I.e.
+`U` - denoted voltage in decivolts
+`I` - current im mA
+`P` - power in deciwatts
+`W` - energy in Wh
+`Pg` - power factor in percents
+`freq` - is frequency in dHz
+`stale` - denotes if data is stale, i.e. has not been updated recently
+`age` - data age in ms
-### ESPEM WebUI Dashboard
+
-
+## Tiered TimeSeries data Sampling
+Controller will keep a history of previous data received from PZEM in it's memory in a tiered memory pool. It is commonly used for time series data where the longer the data age then less frequent is sampling rate of the data to keep. All data resides in volatale `memory`, so it will be lost on power cycle or reset.
+By default there are 3 levels of TimeSeries in a pool
+
+
+
+| Level | Samples count | Sampling interval | Time range |
+| ----- | ------------- | ----------------- | ------------------- |
+| L1 | 900 | 1 sec | 15 min |
+| L2 | 1000 | 15 sec | 250 min (abt 4 hrs) |
+| L3 | 1000 | 300 sec | ~83 hrs |
+
+Number of samples and interval could be adjusted per each level via "Espem setup" - "TimeSeries collector" configuration.
+
+#### ESPEM TS Options
+
+
+
+
+
+#### Averaging
+For levels with sampling interval more that 1 second an averaging function is used to calculate mean value between intervals. I.e. if sampling interval is set to 60 sec and you power level was about 5 watt for 55 seconds and became 100 watts for the last 5 sec when sampling was taken, then resulting power value would be slightly over 5 watts, but not 100 W.
+
+#### Memory consumption
+1000 samples takes about 25 KiB of RAM memory (one sample takes 28 bytes), so plan you pool accordingly. If your board has SPI-RAM then you can have a huge pool worth monthes of data to be kept :)
+
+#### Data export
+TimeSeries Data could be exported in json format per each tier level
+
+Tier 1 URL - [http://espem/samples.json?tsid=1](http://espem/samples.json?tsid=1)
+Tier 2 URL - [http://espem/samples.json?tsid=2](http://espem/samples.json?tsid=2)
+Tier 3 URL - [http://espem/samples.json?tsid=3](http://espem/samples.json?tsid=3)
+
+An example of exported data:
+```
+[
+ {"t":1701876803000,"U":216.60,"I":0.51,"P":88,"W":7,"hz":49.7,"pF":0.79},
+ {"t":1701877103000,"U":225.00,"I":0.52,"P":90,"W":14,"hz":50.1,"pF":0.77}
+]
+```
+where:
+`t` - is a unix timestamp in milliseconds (prefered for js processing)
+other keys are PZEM metrics in float format
+
+
+## Legacy v2.x version
+An older ESPEM version 2 was based on 3rd party lib. It's code still available under [2.x branch](https://github.com/vortigont/espem/tree/v2).
+ESPEM Ver 3 switched to it's own library [pzem-edl](https://github.com/vortigont/pzem-edl). I wrote this lib to overcome limitations of the classic [olehs](https://github.com/olehs/PZEM004T)'s and [mandulaj](https://github.com/mandulaj/PZEM-004T-v30)'s libs. Being versatile those libs provided only basic functions talking to PZEM's using Arduino's blocking IO via serial port. New lib uses event-driven approach and provides extendable design API for multiple PZEM communication over single port.
+
+### Feature comparison
+| | [Version 2.x](https://github.com/vortigont/espem/tree/v2) | Version 3.0 |
+| -------------------- | --------------------------------------------------------- | ----------------- |
+| ESP8266 | YES | NO |
+| ESP32 | YES | YES |
+| PZEM004v30 | YES | YES |
+| old PZEMv30 | YES | NO |
+| PZEM017 (DC version) | NO | NO (planned) |
+| 3 phase option | NO | YES (in progress) |
+| Time Series Charts | YES (Basic) | YES (extendable) |
+| TS in PSRAM | NO | YES |
-### ESPEM WebUI Options
-
### Additional tools
Under /www there is a set of php/sql scripts that could be hosted undel LAMP to gather and calculate stats over long-term periods. Little bit outdated but still usable.
diff --git a/data/index.html.gz b/data/index.html.gz
index c5c5ddf..b0619d5 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 623f75c..b191ed5 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 4d5cb0b..b01e328 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
new file mode 100644
index 0000000..be34b0a
Binary files /dev/null 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
new file mode 100644
index 0000000..342fda6
Binary files /dev/null and b/data/js/lodash.custom.js.gz differ
diff --git a/data/js/tz.json.gz b/data/js/tz.json.gz
index 1b2ae4d..b66da36 100644
Binary files a/data/js/tz.json.gz and b/data/js/tz.json.gz differ
diff --git a/data/js/ui_sys.json.gz b/data/js/ui_sys.json.gz
new file mode 100644
index 0000000..2b35d24
Binary files /dev/null and b/data/js/ui_sys.json.gz differ
diff --git a/espem/espem.cpp b/espem/espem.cpp
index dae9ed9..35c48ef 100644
--- a/espem/espem.cpp
+++ b/espem/espem.cpp
@@ -9,29 +9,34 @@
#include "espem.h"
#include "EmbUI.h" // EmbUI framework
-#ifdef ESP32
- #define MAX_FREE_MEM_BLK ESP.getMaxAllocHeap()
-#else
- #define MAX_FREE_MEM_BLK ESP.getMaxFreeBlockSize()
-#endif
-
-#define PUB_JSSIZE 800
+#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}";
// HTTP responce messages
-static const char PROGMEM PGsmpld[] = "Metrics collector disabled";
-static const char PROGMEM PGdre[] = "Data read error";
-static const char PROGMEM PGacao[] = "Access-Control-Allow-Origin";
+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";
using namespace pzmbus; // use general pzem abstractions
-bool ESPEM::begin(const uart_port_t p, int rx, int tx){
+class FrameSendMQTTRaw : public FrameSendMQTT {
+public:
+ FrameSendMQTTRaw(EmbUI *emb) : FrameSendMQTT(emb){}
+ void send(const JsonVariantConst& data) override {
+ if (data[P_pkg] == C_espem){
+ _eu->publish(C_mqtt_pzem_jmetrics, data[P_block]);
+ }
+ };
+};
+
+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);
// let's make our begin idempotent )
@@ -64,7 +69,7 @@ bool ESPEM::begin(const uart_port_t p, int rx, int tx){
qport->startQueues();
// WebUI updater task
- t_uiupdater.set( DEFAULT_WS_UPD_RATE * TASK_SECOND, TASK_FOREVER, std::bind(&ESPEM::wspublish, this) );
+ t_uiupdater.set( DEFAULT_WS_UPD_RATE * TASK_SECOND, TASK_FOREVER, std::bind(&Espem::wspublish, this) );
ts.addTask(t_uiupdater);
if (pz->autopoll(true)){
@@ -84,8 +89,10 @@ bool ESPEM::begin(const uart_port_t p, int rx, int tx){
});
// generate json with sampled meter data
- embui.server.on(PSTR("/samples"), HTTP_GET, std::bind(&ESPEM::wsamples, this, std::placeholders::_1));
- embui.server.on(PSTR("/samples.json"), HTTP_GET, std::bind(&ESPEM::wsamples, this, std::placeholders::_1));
+ embui.server.on("/samples.json", HTTP_GET, [this](AsyncWebServerRequest *r){ ds.wsamples(r); } );
+
+ // create MQTT rawdata feeder and add into the chain
+ _mqtt_feed_id = embui.feeders.add( std::make_unique(&embui) );
return true;
}
@@ -93,7 +100,7 @@ bool ESPEM::begin(const uart_port_t p, int rx, int tx){
// make a string with last-polled data (cacti poller format)
// this is the 'compat' version for an old pzem w/o pf/HZ values
-String& ESPEM::mktxtdata ( String& txtdata) {
+String& Espem::mktxtdata ( String& txtdata) {
if (!pz)
return txtdata;
@@ -105,7 +112,7 @@ String& ESPEM::mktxtdata ( String& txtdata) {
txtdata += " I:";
txtdata += m->current/1000;
txtdata += " P:";
- txtdata += m->asFloat(meter_t::pwr) + nrg_offset;
+ txtdata += m->asFloat(meter_t::pwr) + ds.getEnergyOffset();
txtdata += " W:";
txtdata += m->asFloat(meter_t::enrg);
// txtdata += " pf:";
@@ -114,9 +121,9 @@ String& ESPEM::mktxtdata ( String& txtdata) {
}
// compat method for v 1.x cacti scripts
-void ESPEM::wpmdata(AsyncWebServerRequest *request) {
- if ( !tsc.getTScnt() ) {
- request->send(503, PGmimetxt, FPSTR(PGdre) );
+void Espem::wpmdata(AsyncWebServerRequest *request) {
+ if ( !ds.getTSsize(1) ) {
+ request->send(503, PGmimetxt, PGdre );
return;
}
@@ -125,7 +132,7 @@ void ESPEM::wpmdata(AsyncWebServerRequest *request) {
}
-void ESPEM::wdatareply(AsyncWebServerRequest *request){
+void Espem::wdatareply(AsyncWebServerRequest *request){
if (!pz)
return;
@@ -136,7 +143,7 @@ void ESPEM::wdatareply(AsyncWebServerRequest *request){
m->asFloat(meter_t::vol),
m->asFloat(meter_t::cur),
m->asFloat(meter_t::pwr),
- m->asFloat(meter_t::enrg) + nrg_offset,
+ m->asFloat(meter_t::enrg) + ds.getEnergyOffset(),
m->asFloat(meter_t::frq),
m->asFloat(meter_t::pf)
);
@@ -145,10 +152,16 @@ void ESPEM::wdatareply(AsyncWebServerRequest *request){
// return json-formatted response for in-RAM sampled data
-void ESPEM::wsamples(AsyncWebServerRequest *request) {
+void DataStorage::wsamples(AsyncWebServerRequest *request) {
+ uint8_t id = 1; // default ts id
+
+ if (request->hasParam("tsid")) {
+ AsyncWebParameter* p = request->getParam("tsid");
+ id = p->value().toInt();
+ }
// check if there is any sampled data
- if ( !tsc.getTScnt() ) {
+ if ( !getTSsize(id) ) {
request->send_P(503, PGmimejson, "[]");
return;
}
@@ -158,14 +171,17 @@ void ESPEM::wsamples(AsyncWebServerRequest *request) {
size_t cnt = 0; // cnt - return last 'cnt' samples, 0 - all samples
- if (request->hasParam("scntr")){
- AsyncWebParameter* p = request->getParam("scntr");
+ if (request->hasParam(C_scnt)){
+ AsyncWebParameter* p = request->getParam(C_scnt);
if (!p->value().isEmpty())
cnt = p->value().toInt();
}
- const auto ts = tsc.getTS(ts_id);
+ const auto ts = getTS(id);
+ if (!ts)
+ request->send_P(503, PGmimejson, "[]");
+
auto iter = ts->cbegin(); // get const iterator
// set number of samples to send in responce
@@ -187,7 +203,7 @@ void ESPEM::wsamples(AsyncWebServerRequest *request) {
size_t len = 0;
if (!index){
- buffer[0] = 0x5b; // Open json with ASCII '['
+ buffer[0] = 0x5b; // Open json array with ASCII '['
++len;
}
@@ -219,32 +235,35 @@ void ESPEM::wsamples(AsyncWebServerRequest *request) {
return len;
});
- response->addHeader(FPSTR(PGacao),"*"); // CORS header
+ response->addHeader(PGacao, "*"); // CORS header
request->send(response);
}
-// publish meter data via WebSocket (a periodic Task)
-void ESPEM::wspublish(){
- if (!embui.ws.count() || !pz) // exit, if there are no clients connected
- return;
+// publish meter data via availbale EmbUI feeders (a periodic Task)
+void Espem::wspublish(){
+ if (!embui.feeders.available() || !pz) // exit, if there are no clients connected
+ return;
const auto m = pz->getMetricsPZ004();
- Interface interf(&embui, &embui.ws, PUB_JSSIZE);
- interf.json_frame("rawdata");
-
- interf.value("stale", pz->getState()->dataStale(), false);
- interf.value("age", pz->getState()->dataAge());
- interf.value("U", m->voltage);
- interf.value("I", m->current);
- interf.value("P", m->power);
- interf.value("W", m->energy + nrg_offset);
- interf.value("Pf", m->pf);
- interf.value("freq", m->freq);
+ 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);
interf.json_frame_flush();
}
-uint8_t ESPEM::set_uirate(uint8_t seconds){
+uint8_t Espem::set_uirate(uint8_t seconds){
if (seconds){
t_uiupdater.setInterval(seconds * TASK_SECOND);
t_uiupdater.restartDelayed();
@@ -254,31 +273,45 @@ uint8_t ESPEM::set_uirate(uint8_t seconds){
return seconds;
}
-uint8_t ESPEM::get_uirate(){
+uint8_t Espem::get_uirate(){
if (t_uiupdater.isEnabled())
return (t_uiupdater.getInterval() / TASK_SECOND);
return 0;
}
-bool ESPEM::tsSet(size_t size, uint32_t interval){
- if (!size || !interval)
- return false;
-
- tsc.purge();
-
- ts_id = tsc.addTS(size, TimeProcessor::getInstance().getUnixTime(), interval, "TS_1");
- //LOG.printf("Add TS: %d\n", sec);
- //tsc.addTS(300, esp_timer_get_time() >> 20, 10, "per10sec", 2);
- //tsc.addTS(300, esp_timer_get_time() >> 20, 60, "permin", 2);
+void DataStorage::reset(){
+ purge();
+ tsids.clear();
+
+ uint8_t a;
+ a = addTS(embui.paramVariant(V_TS_T1_CNT), time(nullptr), embui.paramVariant(V_TS_T1_INT), "Tier 1", 1);
+ tsids.push_back(a);
+ //LOG(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);
+ tsids.push_back(a);
+ //LOG(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);
+ tsids.push_back(a);
+ //LOG(printf, "Add TS: %d\n", a);
+
+ LOG(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));
+ }
+ }
+ )
LOG(printf, "SRAM: heap %u, free %u\n", ESP.getHeapSize(), ESP.getFreeHeap());
- LOG(printf, "SPI-RAM: heap %u, free %u\n", ESP.getPsramSize(), ESP.getFreePsram());
-
- return (bool)tsc.getTScap();
+ LOG(printf, "SPI-RAM: size %u, free %u\n", ESP.getPsramSize(), ESP.getFreePsram());
}
-mcstate_t ESPEM::set_collector_state(mcstate_t state){
+mcstate_t Espem::set_collector_state(mcstate_t state){
if (!pz){
ts_state = mcstate_t::MC_DISABLE;
return ts_state;
@@ -287,15 +320,13 @@ mcstate_t ESPEM::set_collector_state(mcstate_t state){
switch (state) {
case mcstate_t::MC_RUN : {
if (ts_state == mcstate_t::MC_RUN) return mcstate_t::MC_RUN;
- if (!getMetricsCap()) tsSet(); // reinitialize TS Container if empty
+ if (!ds.getTScap()) ds.reset(); // reinitialize TS Container if empty
// attach collector's callback
- auto ref = &tsc;
- pz->attach_rx_callback([this, ref](uint8_t id, const RX_msg* m){
+ pz->attach_rx_callback([this](uint8_t id, const RX_msg* m){
// collect time-series data
if (!pz->getState()->dataStale()){
- auto data = pz->getMetricsPZ004();
- ref->push(*data, TimeProcessor::getInstance().getUnixTime());
+ ds.push(*(pz->getMetricsPZ004()), time(nullptr));
}
#ifdef ESPEM_DEBUG
if (m) msgdebug(id, m); // it will print every data packet coming from PZEM
@@ -306,13 +337,12 @@ mcstate_t ESPEM::set_collector_state(mcstate_t state){
}
case mcstate_t::MC_PAUSE : {
pz->detach_rx_callback();
- if (!getMetricsCap()) tsSet(); // reinitialize TS Container if empty
ts_state = mcstate_t::MC_PAUSE;
break;
}
default: {
pz->detach_rx_callback();
- tsc.purge();
+ ds.purge();
ts_state = mcstate_t::MC_DISABLE;
}
}
diff --git a/espem/espem.h b/espem/espem.h
index 63146df..4d2ff3f 100644
--- a/espem/espem.h
+++ b/espem/espem.h
@@ -19,39 +19,74 @@
#define DEFAULT_WS_UPD_RATE 2 // ws clients update rate, sec
#endif
-#ifndef ESPEM_MEMPOOL
- #define ESPEM_MEMPOOL 300 // samples to store in ringbuff by default (5 min)
-#endif
-
#define PZEM_ID 1
#define PORT_1_ID 1
+#define TS_T1_CNT 900 // default Tier 1 TimeSeries count
+#define TS_T1_INTERVAL 1 // default Tier 1 TimeSeries interval (1 sec)
+#define TS_T2_CNT 1000 // default Tier 2 TimeSeries count
+#define TS_T2_INTERVAL 15 // default Tier 2 TimeSeries interval (15 sec)
+#define TS_T3_CNT 1000 // default Tier 3 TimeSeries count
+#define TS_T3_INTERVAL 300 // default Tier 3 TimeSeries interval (5 min)
+
+
// Metrics collector state
enum class mcstate_t{MC_DISABLE=0, MC_RUN, MC_PAUSE};
// TaskScheduler - Let the runner object be a global, single instance shared between object files.
extern Scheduler ts;
-class ESPEM {
+class DataStorage : public TSContainer {
+ std::vector tsids;
+
+ // energy offset
+ int32_t nrg_offset{0};
+
public:
- //std::unique_ptr pzpool; // PZEM object
- PZ004 *pz = nullptr;
+ /**
+ * @brief setup TimeSeries Container based on saved params in EmbUI config
+ *
+ */
+ void reset();
/**
- * Class constructor
- * uses predefined values of a ESPEM_CFG
+ * @brief Set the Energy offset value
+ * tis will offset energy value replies from PZEM
+ * i.e. to match some other counter, etc...
+ *
+ * @param offset
*/
- ESPEM(){}
+ void setEnergyOffset(int32_t offset){ nrg_offset = offset; }
+
+ /**
+ * @brief Get the Energy offset value
+ *
+ * @return float
+ */
+ int32_t getEnergyOffset(){ return nrg_offset; }
+
+ void wsamples(AsyncWebServerRequest *request);
+
+};
+
+class Espem {
+
+public:
+
+ PZ004 *pz = nullptr;
+
+ // TimeSeries data storage
+ DataStorage ds;
/**
* Class constructor
- * initialized with customized ESPEM_CFG
+ * uses predefined values of a ESPEM_CFG
*/
- //ESPEM(const ESPEM_CFG& _cfg) : ecfg(_cfg){}
+ Espem(){}
- ~ESPEM(){
+ ~Espem(){
ts.deleteTask(t_uiupdater);
delete pz;
pz = nullptr;
@@ -82,15 +117,18 @@ class ESPEM {
void wpmdata(AsyncWebServerRequest *request);
- void wsamples(AsyncWebServerRequest *request);
-
/**
* @brief - set webUI refresh rate in seconds
* @param seconds - webUI interval
*/
uint8_t set_uirate(uint8_t seconds);
- uint8_t get_uirate();
+ /**
+ * @brief Get the ui refresh rate
+ *
+ * @return uint8_t
+ */
+ uint8_t get_uirate(); // TaskScheduler class does not allow it to declare const'ness
/**
@@ -101,52 +139,18 @@ class ESPEM {
bool meterPolling(bool active){ return pz->autopoll(active); };
bool meterPolling() const { return pz->autopoll(); };
- /**
- * @brief - get metrics storage capacity, if any
- *
- */
- int getMetricsCap() const { return tsc.getTScap(); }
-
- int getMetricsSize() const { return tsc.getTSsize(); }
-
-
- /**
- * @brief set TimeSeries Container params
- *
- * @param size - number of elements to store
- * @param interval - series interval in seconds
- * @return true - on success
- * @return false - on error
- */
- bool tsSet(size_t size = ESPEM_MEMPOOL, uint32_t interval = 1);
-
mcstate_t set_collector_state(mcstate_t state);
mcstate_t get_collector_state() const { return ts_state; };
- /**
- * @brief Set the Energy offset value
- * tis will offset energy value replies from PZEM
- * i.e. to match some other counter, etc...
- *
- * @param offset
- */
- inline void setEnergyOffset(float offset){ nrg_offset = offset; };
-
- /**
- * @brief Get the Energy offset value
- *
- * @return float
- */
- inline float getEnergyOffset(){return nrg_offset;};
-
private:
UartQ *qport = nullptr;
- TSContainer tsc;
- uint8_t ts_id;
mcstate_t ts_state = mcstate_t::MC_DISABLE;
+ // Tasks
+ Task t_uiupdater;
- float nrg_offset{0.0};
+ // mqtt feeder id
+ int _mqtt_feed_id{0};
String& mktxtdata ( String& txtdata);
@@ -156,8 +160,6 @@ class ESPEM {
*/
void wspublish();
- // Tasks
- Task t_uiupdater;
// make json string out of array provided
// bool W - include energy counter in json
@@ -173,4 +175,5 @@ class ESPEM {
* @param id
* @param m
*/
-void msgdebug(uint8_t id, const RX_msg* m);
\ No newline at end of file
+void msgdebug(uint8_t id, const RX_msg* m);
+
diff --git a/espem/interface.cpp b/espem/interface.cpp
index 899ee1a..27446dc 100644
--- a/espem/interface.cpp
+++ b/espem/interface.cpp
@@ -8,67 +8,15 @@
#define MAX_UI_UPDATE_RATE 30
-extern ESPEM *espem;
+extern Espem *espem;
static const char* chart_css = "graphwide";
-/**
- * Define configuration variables and controls handlers
- * variables has literal names and are kept within json-configuration file on flash
- *
- * Control handlers are bound by literal name with a particular method. This method is invoked
- * by manipulating controls
- *
- */
-void create_parameters(){
- LOG(println, "UI: Creating application vars");
-
- /**
- * регистрируем свои переменные
- */
- embui.var_create(V_UI_UPDRT, DEFAULT_WS_UPD_RATE); // WebUI update rate
- embui.var_create(V_SMPL_PERIOD, 1); //
- embui.var_create(V_EPOOLSIZE, ESPEM_MEMPOOL); // metrics collector mem pool size, KiB
- 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.0); // Energy counter offset
-
-
- //Metrics collector run/pause
- //embui.var_create(V_ECOLLECTORSTATE, 1); // Collector state
-
- /**
- * обработчики действий
- */
- // вывод WebUI секций
- embui.section_handle_add(B_ESPEM, block_page_main); // generate "main" info page
- embui.section_handle_add(B_ESPEMSET, block_page_espemset); // generate "ESPEM settings" page
-
-
- /**
- * регистрируем статические секции для web-интерфейса с системными настройками
- */
- basicui::add_sections();
-
-
- // активности
- embui.section_handle_add(A_SET_ESPEM, set_sampler_opts);
- embui.section_handle_add(A_SET_UART, set_uart_opts);
- embui.section_handle_add(A_SET_PZOPTS, set_pzopts);
-
- // direct controls
- embui.section_handle_add(A_DIRECT_CTL, set_directctrls); // process direct update controls
-
-
-}
+// variable that holds TS id which is currently displayed at Web UI
+unsigned power_chart_id{1};
-
-/**
- * Sync all UI params
- */
-void sync_parameters(){}
+// forward declarations
+void ui_page_espem(Interface *interf, const JsonObject *data, const char* action);
/**
* Headlile section
@@ -78,116 +26,179 @@ void sync_parameters(){}
* переопределенный метод фреймфорка, который начинает строить корень нашего WebUI
*
*/
-void section_main_frame(Interface *interf, JsonObject *data){
- if (!interf) return;
+void ui_page_main(Interface *interf, const JsonObject *data, const char* action){
interf->json_frame_interface();
// application manifest
- interf->json_section_manifest(C_DICT[lang][CD::ESPEM_H], 0, FW_VERSION_STRING); // HEADLINE for WebUI
+ 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
- block_menu(interf, data); // Строим UI блок с меню выбора других секций
- interf->json_frame_flush(); // send frame
+ // load uidata objects
+ interf->json_section_uidata();
+ interf->uidata_xload(C_espem_ui, "js/espem.ui.json", false, ESPEM_UI_VERSION);
+ interf->json_section_end();
- if(!(WiFi.getMode() & WIFI_MODE_STA)){ // если контроллер не подключен к внешней AP, сразу открываем вкладку с настройками WiFi
- LOG(println, "UI: Opening network setup section");
- basicui::block_settings_netw(interf, data);
+ block_menu(interf); // Строим UI блок с меню выбора других секций
+ interf->json_frame_flush(); // send frame
+
+ if((WiFi.getMode() & WIFI_MODE_STA)){ // если контроллер не подключен к внешней AP, сразу открываем вкладку с настройками WiFi
+ ui_page_espem(interf, nullptr, action); // construct main page
} else {
- block_page_main(interf, data); // Строим основной блок
+ basicui::page_settings_netw(interf, nullptr, NULL);
}
-
}
/**
* This code builds UI section with menu block on the left
*
*/
-void block_menu(Interface *interf, JsonObject *data){
- if (!interf) return;
+void block_menu(Interface *interf){
// создаем меню
interf->json_section_menu(); // открываем секцию "меню"
- interf->option(B_ESPEM, C_DICT[lang][CD::ESPEM_DB]); // пункт меню "ESPEM Info"
- interf->option(B_ESPEMSET, C_DICT[lang][CD::ESPEMSet]); // пункт меню "ESPEM Setup"
+ 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"
/**
* добавляем в меню пункт - настройки,
*/
- basicui::opt_setup(interf, data); // пункт меню "настройки"
-
+ basicui::menuitem_settings(interf);
interf->json_section_end();
}
+/**
+ * @brief create and send jscall frame thet will trigger building power-chart on WebUI
+ *
+ * @param 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;
+ params["interval"] = espem->ds.getTS(power_chart_id)->getInterval();
+ params[C_scnt] = embui.paramVariant(V_SMPLCNT).as(); // espem->ds.getTScap(power_chart_id); // samples counter
+ interf->jobject(params, true);
+ interf->json_frame_flush(); // flush frame
+}
+
+/**
+ * @brief create/replace UI section with power chart controls
+ *
+ * @param interf
+ */
+void ui_block_chart_ctrls(Interface *interf){
+ interf->json_section_line(C_lchart); // chart Live controls
+ interf->select(A_TS_TIER, power_chart_id, "TimeSeries Interval", true);
+ for (unsigned i = 0; i != espem->ds.getTScnt(); ++i){
+ String lbl(espem->ds.getTS(i+1)->getInterval());
+ lbl += " sec.";
+ interf->option(i+1, lbl); // ids are starting from 1
+ }
+ 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);
+ interf->json_section_end(); // end of line
+}
+
/**
* This code builds UI section with dashboard
*
*/
-void block_page_main(Interface *interf, JsonObject *data){
- if (!interf) return;
+void ui_page_espem(Interface *interf, const JsonObject *data, const char* action){
interf->json_frame_interface();
- interf->json_section_main(B_ESPEM, C_DICT[lang][CD::ESPEM_H]);
+ interf->json_section_main(A_ui_page_espem, C_DICT[lang][CD::ESPEM_H]);
interf->json_section_line(); // "Live controls"
-
- interf->checkbox(V_EPOLLENA, (bool)espem->get_uirate(), "Live update", true); // Meter poller status
- // UI update rate range slider
- interf->range(V_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
+ 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
+ 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
- StaticJsonDocument<64> doc;
- JsonObject params = doc.to(); // parameters for charts
-
interf->json_section_line();
- // id, type, value, label, param
- interf->jscall("gaugeV", C_mkchart, C_DICT[lang][CD::Voltage], chart_css); // Voltage gauge
- interf->jscall("gaugePF", C_mkchart, C_DICT[lang][CD::PowerF], chart_css); // Power Factor
+ // 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
- params["arg1"] = embui.paramVariant(V_SMPLCNT); // samples counter
- interf->jscall("gsmini", C_mkchart, "Power chart", chart_css, params); // Power chart
+ interf->spacer("Power chart");
+ ui_block_chart_ctrls(interf);
- // slider for the amount of metric samples to be plotted on a chart
- interf->range(V_SMPLCNT, embui.paramVariant(V_SMPLCNT).as(), 0, (int)espem->getMetricsCap(), 10, C_DICT[lang][CD::MScale], true);
+ // empty div placeholder for TimeSeries Power chart
+ interf->jscall(C_gsmini, P_EMPTY, P_EMPTY, chart_css);
interf->json_frame_flush(); // flush frame
+
+ // 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){
+ interf->button(button_t::generic, A_ui_page_espem_setup, "ESPEM");
}
/**
* ESPEM options setup
*
*/
-void block_page_espemset(Interface *interf, JsonObject *data){
- if (!interf) return;
+void block_page_espemset(Interface *interf, const JsonObject *data, const char* action){
interf->json_frame_interface();
+ interf->json_section_uidata();
+ interf->uidata_pick("espem.ui.settings.cfg");
+ 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_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());
+ // collector state
+ interf->value(A_ECOLLECTORSTATE, (uint8_t)espem->get_collector_state());
+ interf->json_frame_flush();
- // replacing page with a new one with settings
- //interf->json_section_main(A_SET_ESPEM, C_DICT[lang][CD::ESPEMSet]);
- interf->json_section_main("", C_DICT[lang][CD::ESPEMSet]);
- // Poller Line block
- /*
- interf->json_section_line("");
- interf->checkbox(V_EPOLLENA, espem->meterPolling(), "Meter Polling", true); // Meter poller status
+ interf->json_frame_interface();
+ interf->json_section_content();
+
+ 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(key,8, "t%umem", i);
+ interf->constant(std::string_view(key), std::string_view(buff)); // capacity and memory usage
+ }
- // UI update Rate range slider
- interf->range(V_UI_UPDRT, embui.paramVariant(V_UI_UPDRT).as(), 0, MAX_UI_UPDATE_RATE, 1, "UI refresh rate, sec", true);
- interf->json_section_end(); // end of line
- */
+ interf->json_frame_flush();
- interf->json_section_begin(A_SET_UART);
+/*
+ // 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);
@@ -200,7 +211,7 @@ void block_page_espemset(Interface *interf, JsonObject *data){
// counter opts
interf->spacer("Energy counter options");
interf->json_section_begin(A_SET_PZOPTS);
- interf->number(V_EOFFSET, espem->getEnergyOffset(), "Energy counter offset");
+ 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"
@@ -208,58 +219,61 @@ void block_page_espemset(Interface *interf, JsonObject *data){
interf->spacer("Metrics collector options");
String _msg("Metrics pool capacity: ");
- _msg += espem->getMetricsSize();
+ _msg += espem->ds.getMetricsSize();
_msg += "/";
- _msg += espem->getMetricsCap(); // current number of metrics samples
+ _msg += espem->ds.getMetricsCap(); // current number of metrics samples
_msg += " samples";
interf->constant("mcap", _msg);
- interf->json_section_line(A_SET_ESPEM);
- interf->number(V_EPOOLSIZE, embui.paramVariant(V_EPOOLSIZE).as(), "RAM pool size, samples"); // Memory pool for metrics data, samples
- interf->number(V_SMPL_PERIOD, embui.paramVariant(V_SMPL_PERIOD).as(), "Sampling period"); // sampling period, sec
- interf->json_section_end(); // end of line
// Button "Apply Metrics pool settings"
- interf->button(button_t::submit, A_SET_ESPEM, 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(V_ECOLLECTORSTATE, (uint8_t)espem->get_collector_state(), "Metrics collector status", true);
+ 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 pubCallback(Interface *interf){
- basicui::embuistatus(interf);
+void ui_page_dataexport(Interface *interf, const JsonObject *data, const char* action){
+ interf->json_frame_interface();
+ interf->json_section_uidata();
+ interf->uidata_pick("espem.ui.export");
+ interf->json_frame_flush();
}
-
// Callback ACTIONS
/**
* Apply espem options values
*/
-void set_sampler_opts(Interface *interf, JsonObject *data){
+void set_sampler_opts(Interface *interf, const JsonObject *data, const char* action){
if (!data) return;
-
- SETPARAM(V_EPOOLSIZE);
- SETPARAM(V_SMPL_PERIOD);
-
- espem->tsSet((*data)[V_EPOOLSIZE].as(), (*data)[V_SMPL_PERIOD].as());
+ // 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);
+
+ espem->ds.reset();
// display main page
- if (interf) block_page_main(interf, nullptr);
+ if (interf) ui_page_espem(interf, nullptr, NULL);
}
@@ -267,68 +281,71 @@ void set_sampler_opts(Interface *interf, JsonObject *data){
* обработка "живых" переключателей
*
*/
-void set_directctrls(Interface *interf, JsonObject *data){
+void set_directctrls(Interface *interf, const JsonObject *data, const char* action){
if (!data) return;
- for (JsonPair kv : (*data)) {
-
- //LOG(printf_P, PSTR("Iterating Key:%s Value:%s\n"), kv.key().c_str(), kv.value().as() );
-
- String _s(V_EPFFIX);
- String _k(kv.key().c_str());
+ std::string_view sv(action);
+ sv.remove_prefix(5); // 'dctl_'
- _s=V_EPOLLENA;
- if (!_s.compareTo(_k)){
- if(kv.value()){
- espem->set_uirate(embui.paramVariant(V_UI_UPDRT));
- } else {
- espem->set_uirate(0);
- }
- LOG(printf_P, PSTR("ESPEM: UI refresh state: %d\n"), kv.value().as() );
- continue;
- }
+ // ena/disable polling
+ if (sv.compare(V_EPOLLENA) == 0){
+ espem->set_uirate( (*data)[action] ? embui.paramVariant(V_UI_UPDRT) : 0);
+ LOG(printf_P, PSTR("ESPEM: UI refresh state: %d\n"), (*data)[A_EPOLLENA].as() );
+ return;
+ }
- _s=V_UI_UPDRT;
- if (!_s.compareTo(_k)){
- espem->set_uirate(kv.value().as());
- SETPARAM(V_UI_UPDRT);
- LOG( printf_P, PSTR("ESPEM: Set UI update rate to: %d\n"), espem->get_uirate() );
- continue;
- }
+ // 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_P, PSTR("ESPEM: 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];
+ // 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));
- _s=V_ECOLLECTORSTATE;
- if (!_s.compareTo(_k)){
- uint8_t new_state = kv.value().as();
- // reset TS Container if empty and we need to start it
- if (espem->get_collector_state() == mcstate_t::MC_DISABLE && new_state >0) espem->tsSet(embui.paramVariant(V_EPOOLSIZE), embui.paramVariant(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() );
+ return;
+ }
- espem->set_collector_state((mcstate_t)new_state);
- //embui.var(_k, (uint8_t)espem->set_collector_state((mcstate_t)new_state), true); // no need to set this var, it's a run-time state
- LOG(printf_P, PSTR("UI: Set TS Collector state to: %d\n"), (int)espem->get_collector_state() );
- continue;
+ // Metrics graph - number of samples to draw in a small power chart
+ if (sv.compare(C_scnt) == 0){
+ embui.var(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_flush();
}
+ return;
+ }
+ if (sv.compare(C_tier) == 0){
+ // save new TS id
+ power_chart_id = (*data)[action];
+ if (!interf) return;
- // Set amount of samples displayed on chart (TODO: replace with js internal var)
- _s=V_SMPLCNT;
- if (!_s.compareTo(_k)){
- SETPARAM(V_SMPLCNT);
+ // call js function to build power chart
+ ui_frame_mkchart(interf);
- if (interf){
- interf->json_frame("rawdata");
- interf->value("scntr", kv.value());
- interf->json_frame_flush();
- }
- }
+ // send update command to AmCharts block
+ interf->json_frame_interface();
+ ui_block_chart_ctrls(interf);
+ interf->json_frame_flush();
+ return;
}
-
}
/**
* Apply uart options values
*/
-void set_uart_opts(Interface *interf, JsonObject *data){
+void set_uart_opts(Interface *interf, const JsonObject *data, const char* action){
if (!data) return;
uint8_t p = (*data)[V_UART].as();
@@ -348,7 +365,7 @@ void set_uart_opts(Interface *interf, JsonObject *data){
espem->begin(embui.paramVariant(V_UART), embui.paramVariant(V_RX), embui.paramVariant(V_TX));
// display main page
- if (interf) block_page_main(interf, nullptr);
+ if (interf) ui_page_espem(interf, nullptr, NULL);
}
/**
@@ -357,12 +374,64 @@ void set_uart_opts(Interface *interf, JsonObject *data){
* @param interf
* @param data
*/
-void set_pzopts(Interface *interf, JsonObject *data){
+void set_pzopts(Interface *interf, const JsonObject *data, const char* action){
if (!data) return;
SETPARAM(V_EOFFSET);
- espem->setEnergyOffset(embui.paramVariant(V_EOFFSET));
+ espem->ds.setEnergyOffset(embui.paramVariant(V_EOFFSET));
// display main page
- if (interf) block_page_main(interf, nullptr);
+ if (interf) ui_page_espem(interf, nullptr, NULL);
+}
+
+
+/**
+ * Define configuration variables and controls handlers
+ * variables has literal names and are kept within json-configuration file on flash
+ *
+ * Control handlers are bound by literal name with a particular method. This method is invoked
+ * by manipulating controls
+ *
+ */
+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
+
+ /**
+ * обработчики действий
+ */
+
+ // 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_publish_cb(pubCallback); // Publish callback
+
+ // вывод WebUI секций
+ embui.action.add(A_ui_page_espem, ui_page_espem); // generate "main" info page
+ embui.action.add(A_ui_page_espem_setup, block_page_espemset); // generate "ESPEM settings" page
+ embui.action.add(A_ui_page_dataexport, ui_page_dataexport); // generate "Data export" page
+
+ // активности
+ embui.action.add(A_SET_MCOLLECTOR, set_sampler_opts); // set options for TimeSeries collector
+ embui.action.add(A_SET_UART, set_uart_opts); // set UART gpios
+ embui.action.add(A_SET_PZOPTS, set_pzopts); // set options for PZEM (egergy offset)
+
+ // direct controls
+ embui.action.add(A_DIRECT_CTL, set_directctrls); // process onChange update controls
}
diff --git a/espem/interface.h b/espem/interface.h
index 15ea38d..1a469bc 100644
--- a/espem/interface.h
+++ b/espem/interface.h
@@ -1,19 +1,19 @@
#pragma once
-// Interface blocks
-void block_menu(Interface *interf, JsonObject *data);
-void block_page_main(Interface *interf, JsonObject *data);
-void block_page_espemset(Interface *interf, JsonObject *data);
+// register config params and action callbacks
+void embui_actions_register();
-//void remote_action(RA action, ...);
-//void uploadProgress(size_t len, size_t total);
+// 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);
// ACTIONS
-void action_demopage(Interface *interf, JsonObject *data);
-void set_sampler_opts(Interface *interf, JsonObject *data);
-void set_directctrls(Interface *interf, JsonObject *data);
-void set_uart_opts(Interface *interf, JsonObject *data);
-void set_pzopts(Interface *interf, JsonObject *data);
+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);
// Callbacks
void pubCallback(Interface *interf);
diff --git a/espem/main.cpp b/espem/main.cpp
index 8ea8893..bef7c1d 100644
--- a/espem/main.cpp
+++ b/espem/main.cpp
@@ -11,6 +11,7 @@
#include "espem.h"
#include
+#include "interface.h"
extern "C" int clock_gettime(clockid_t unused, struct timespec *tp);
@@ -20,7 +21,7 @@ extern "C" int clock_gettime(clockid_t unused, struct timespec *tp);
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}";
// Our instance of espem
-ESPEM *espem = nullptr;
+Espem *espem = nullptr;
// ----
// MAIN Setup
@@ -34,23 +35,28 @@ void setup() {
// Start framework, load config and connect WiFi
embui.begin();
+ embui_actions_register();
// create and run ESPEM object
- espem = new ESPEM();
+ espem = new Espem();
- if (espem && espem->begin(embui.paramVariant(FPSTR(V_UART)), embui.paramVariant(FPSTR(V_RX)), embui.paramVariant(FPSTR(V_TX))) ){
- if ( espem->tsSet( embui.paramVariant(FPSTR(V_EPOOLSIZE)), embui.paramVariant(FPSTR(V_SMPL_PERIOD)) ) ){
- espem->set_collector_state(mcstate_t::MC_RUN);
- }
- espem->setEnergyOffset(embui.paramVariant(FPSTR(V_EOFFSET)));
- }
+ if (espem && espem->begin( embui.paramVariant(V_UART),
+ embui.paramVariant(V_RX),
+ embui.paramVariant(V_TX))
+ )
+ {
+ espem->ds.setEnergyOffset(embui.paramVariant(V_EOFFSET));
- embui.server.on(PSTR("/fw"), HTTP_GET, [](AsyncWebServerRequest *request){
- wver(request);
- });
+ // postpone TimeSeries setup until NTP aquires valid time
+ TimeProcessor::getInstance().attach_callback([](){
+ espem->set_collector_state(mcstate_t::MC_RUN);
+ // we only need that setup once
+ TimeProcessor::getInstance().dettach_callback();
+ });
+ }
- //sync_parameters(); // sync UI params
+ embui.server.on("/fw", HTTP_GET, [](AsyncWebServerRequest *request){ wver(request); });
embui.setPubInterval(WEBUI_PUBLISH_INTERVAL);
}
diff --git a/espem/main.h b/espem/main.h
index 4d0cbed..49227e0 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 - 2022
+ * (c) Emil Muratov 2018 - 2023
*
*/
@@ -11,7 +11,7 @@
#define FW_NAME "espem"
#define FW_VERSION_MAJOR 3
-#define FW_VERSION_MINOR 1
+#define FW_VERSION_MINOR 2
#define FW_VERSION_REVISION 0
/* make version as integer*/
@@ -20,6 +20,8 @@
/* make version as string*/
#define FW_VERSION_STRING TOSTRING(FW_VERSION_MAJOR) "." TOSTRING(FW_VERSION_MINOR) "." TOSTRING(FW_VERSION_REVISION)
+#define ESPEM_JSAPI_VERSION 2
+#define ESPEM_UI_VERSION 2
#define BAUD_RATE 115200 // serial debug port baud rate
diff --git a/espem/uistrings.h b/espem/uistrings.h
index e99e71c..1a5ef73 100644
--- a/espem/uistrings.h
+++ b/espem/uistrings.h
@@ -3,40 +3,57 @@
// Set of flash-strings that might be reused multiple times within the code
// General
-static constexpr const char C_ONE[] PROGMEM = "1";
-static constexpr const char C_mkchart[] PROGMEM = "mkchart";
-static constexpr const char C_js[] PROGMEM = "js";
+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";
+
//////////////////////
// Configuration variables names - V_ prefix for 'Variable'
-static constexpr const char V_EPOOLSIZE[] = "emplsz"; // sample pool size
-static constexpr const char V_SMPL_PERIOD[] = "ems_prd"; // sampling period
-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
-static constexpr const char V_EPOLLENA[] PROGMEM = "dctlpoll"; // Enable/disable poller
-static constexpr const char V_EPFFIX[] PROGMEM = "dctlpffix"; // PowerFactor value correction
-static constexpr const char V_UI_UPDRT[] = "dctlupdrt"; // UI update rate
-static constexpr const char V_ECOLLECTORSTATE[] = "dctlmtrx"; // Metrics collector run/pause
-static constexpr const char V_SMPLCNT[] = "dctlscnt"; // Metrics graph - number of samples to draw in a small power chart
+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
// UI blocks - B_ prefix for 'web Block'
-static constexpr const char B_ESPEM[] PROGMEM = "b_espem";
-static constexpr const char B_ESPEMSET[] PROGMEM = "b_emset";
-static constexpr const char B_WEATHER[] PROGMEM = "b_wthr";
-static constexpr const char B_MORE[] PROGMEM = "b_more";
+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[] PROGMEM = "dctl*"; // checkboxes/controls that should be processed in real-time
-
+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[] PROGMEM = "a_setem"; // ESPEM settings update
-static constexpr const char A_SET_UART[] PROGMEM = "a_uart";
-static constexpr const char A_SET_PZOPTS[] PROGMEM = "a_pzopt";
-
+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
// other constants
diff --git a/examples/mqtt.png b/examples/mqtt.png
new file mode 100644
index 0000000..7cb6274
Binary files /dev/null and b/examples/mqtt.png differ
diff --git a/examples/ts_setup.png b/examples/ts_setup.png
new file mode 100644
index 0000000..c07fda8
Binary files /dev/null and b/examples/ts_setup.png differ
diff --git a/platformio.ini b/platformio.ini
index 0e8e8e4..d782dce 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -18,7 +18,7 @@ build_src_flags =
src_build_unflags =
-std=gnu++11
lib_deps =
- https://github.com/vortigont/EmbUI.git#v2.8.1
+ https://github.com/vortigont/EmbUI.git#v3.1
lib_ignore =
ESPAsyncTCP
monitor_speed = 115200
@@ -52,7 +52,7 @@ all_serial1 =
extends = common
platform = espressif32
board = wemos_d1_mini32
-upload_speed = 460800
+;upload_speed = 460800
monitor_filters = esp32_exception_decoder
;build_flags =
lib_ignore =
diff --git a/resources/html/index.html b/resources/html/index.html
index d7d8310..518f727 100644
--- a/resources/html/index.html
+++ b/resources/html/index.html
@@ -19,6 +19,9 @@
+
Web-ресурсы на файловой системе требуют обновления. Пожалуйста, обновите файлы из репозитория проекта
+ Web-resouce files on filesystem are outdated. Please, update FS content from project's repository
+
@@ -48,6 +51,7 @@
Time:{{value.pTime}}
Memory:{{value.pMem}}
Uptime:{{value.pUptime}}
+
RSSI 📶:{{value.pRSSI}}
ChipID:{{macid}}
EmbUI ver:{{uiver}}
FirmWare ver:{{appver}}
@@ -100,26 +104,26 @@
{{/if}}
{{#if type == "checkbox"}}
-
+
{{/if}}
{{#if html == "const"}}
-
{{label}}
- {{#if2 value}}{{/if2}}
+
{{label}}{{#if2 value}}{{/if2}}
{{/if}}
{{#if html == "comment"}}
{{label}}
{{/if}}
{{#if html == "div" && type == "js"}}
- {{#if2 label}}{{label}}{{/if2}}
-
+ {{#if2 label}}{{label}}{{/if2}}
+
{{/if}}
{{#if html == "div" && type == "html"}}
- {{#if2 label}}{{label}}{{/if2}}
-
{{value}}
+ {{#if2 label}}{{label}}{{/if2}}
+
{{value}}
{{/if}}
{{#if html == "div" && type == "pbar"}}
@@ -153,23 +157,23 @@
{{/if}}
{{#if html == "input" && type != "checkbox"}}
- {{label}}
+ {{label}}
{{#if2 type == "range" || type == "color"}}: {{value}}{{/if2}}
- {{#if2 type == "text" || type == "password"}} ({{calc value.length || 0}}){{/if2}}
+ {{#if2 type == "text" || type == "password"}} ({{calc value.length || 0}}){{/if2}}
{{/if}}
{{#if html == "select"}}
{{label}}