diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..63f29c0b --- /dev/null +++ b/.clang-format @@ -0,0 +1,22 @@ +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignConsecutiveMacros: true +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Inline +BinPackArguments: false +ColumnLimit: 0 +ContinuationIndentWidth: 2 +FixNamespaceComments: false +IndentAccessModifiers: true +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +IndentWidth: 2 +NamespaceIndentation: All +PointerAlignment: Left +ReferenceAlignment: Left +TabWidth: 2 +UseTab: Never diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5976c27e..a235d19e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,12 @@ jobs: core: esp32:esp32 board: esp32:esp32:esp32 index_url: https://espressif.github.io/arduino-esp32/package_esp32_index.json - - name: package_esp32_dev_index.json - core: esp32:esp32 - board: esp32:esp32:esp32 - index_url: https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json + # Disabled due to an error coming from the package file: + # Error during install: Cannot install tool esp32:esp32-arduino-libs@idf-release_v5.3-a0f798cf: testing local archive integrity: testing archive checksum: archive hash differs from hash in index + # - name: package_esp32_dev_index.json + # core: esp32:esp32 + # board: esp32:esp32:esp32 + # index_url: https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json - name: package_esp8266com_index.json core: esp8266:esp8266 board: esp8266:esp8266:huzzah @@ -62,10 +64,10 @@ jobs: run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/mathieucarbou/esphome-ESPAsyncTCP#v2.0.0 - name: Install ESPAsyncWebServer - run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/mathieucarbou/ESPAsyncWebServer#v3.3.14 + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/mathieucarbou/ESPAsyncWebServer#v3.3.23 - name: Install ArduinoJson - run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/bblanchon/ArduinoJson#v7.1.0 + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/bblanchon/ArduinoJson#v7.2.1 - name: Build AccessPoint run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/AccessPoint/AccessPoint.ino" @@ -76,6 +78,9 @@ jobs: - name: Build Benchmark run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/Benchmark/Benchmark.ino" + - name: Build Benchmark5 + run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/Benchmark5/Benchmark5.ino" + - name: Build Chart run: arduino-cli compile --library . --warnings none -b ${{ matrix.board }} "examples/Chart/Chart.ino" @@ -152,6 +157,7 @@ jobs: - run: PLATFORMIO_SRC_DIR=examples/AccessPoint PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - run: PLATFORMIO_SRC_DIR=examples/Basic PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - run: PLATFORMIO_SRC_DIR=examples/Benchmark PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} + - run: PLATFORMIO_SRC_DIR=examples/Benchmark5 PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - run: PLATFORMIO_SRC_DIR=examples/Chart PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - run: PLATFORMIO_SRC_DIR=examples/Dynamic PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - run: PLATFORMIO_SRC_DIR=examples/Interactive PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} diff --git a/examples/Benchmark/Benchmark.ino b/examples/Benchmark/Benchmark.ino index 48c8bb99..b817de02 100644 --- a/examples/Benchmark/Benchmark.ino +++ b/examples/Benchmark/Benchmark.ino @@ -42,10 +42,10 @@ ESPDash dashboard(&server); Card generic(&dashboard, GENERIC_CARD, "Generic"); Card temp(&dashboard, TEMPERATURE_CARD, "Temperature", "°C"); Card hum(&dashboard, HUMIDITY_CARD, "Humidity", "%"); -Card status1(&dashboard, STATUS_CARD, "Status 1", "success"); -Card status2(&dashboard, STATUS_CARD, "Status 2", "warning"); -Card status3(&dashboard, STATUS_CARD, "Status 3", "danger"); -Card status4(&dashboard, STATUS_CARD, "Status 4", "idle"); +Card status1(&dashboard, STATUS_CARD, "Status 1", DASH_STATUS_SUCCESS); +Card status2(&dashboard, STATUS_CARD, "Status 2", DASH_STATUS_WARNING); +Card status3(&dashboard, STATUS_CARD, "Status 3", DASH_STATUS_DANGER); +Card status4(&dashboard, STATUS_CARD, "Status 4", DASH_STATUS_IDLE); Card progress(&dashboard, PROGRESS_CARD, "Progress", "", 0, 100); Card button(&dashboard, BUTTON_CARD, "Test Button"); Card slider(&dashboard, SLIDER_CARD, "Test Slider", "", 0, 255); @@ -53,7 +53,7 @@ Card slider(&dashboard, SLIDER_CARD, "Test Slider", "", 0, 255); Chart bar(&dashboard, BAR_CHART, "Power Usage (kWh)"); // Bar Chart Data -String XAxis[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +dash::string XAxis[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; int YAxis[] = {0, 0, 0, 0, 0, 0, 0}; @@ -61,14 +61,17 @@ void setup() { Serial.begin(115200); /* Connect WiFi */ - WiFi.mode(WIFI_STA); - WiFi.begin(ssid, password); - if (WiFi.waitForConnectResult() != WL_CONNECTED) { - Serial.printf("WiFi Failed!\n"); - return; - } - Serial.print("IP Address: "); - Serial.println(WiFi.localIP()); + // WiFi.mode(WIFI_STA); + // WiFi.begin(ssid, password); + // if (WiFi.waitForConnectResult() != WL_CONNECTED) { + // Serial.printf("WiFi Failed!\n"); + // return; + // } + // Serial.print("IP Address: "); + // Serial.println(WiFi.localIP()); + + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); bar.updateX(XAxis, 7); @@ -107,10 +110,10 @@ void loop() { generic.update((int)random(0, 100)); temp.update((int)random(0, 100)); hum.update((int)random(0, 100)); - status1.update("success"); - status2.update("warning"); - status3.update("danger"); - status4.update("idle"); + status1.update("message 1", DASH_STATUS_SUCCESS); + status2.update("message 2", DASH_STATUS_WARNING); + status3.update("message 3", DASH_STATUS_DANGER); + status4.update("message 4", DASH_STATUS_IDLE); progress.update((int)random(0, 100)); dashboard.sendUpdates(); diff --git a/examples/Benchmark5/Benchmark5.ino b/examples/Benchmark5/Benchmark5.ino new file mode 100644 index 00000000..279028d2 --- /dev/null +++ b/examples/Benchmark5/Benchmark5.ino @@ -0,0 +1,200 @@ +/* + ----------------------------- + ESPDASH Pro - Benchmark Example + ----------------------------- + Use this benchmark example to test if ESP-DASH Pro is working properly on your platform. + + Github: https://github.com/ayushsharma82/ESP-DASH + WiKi: https://docs.espdash.pro + + Works with both ESP8266 & ESP32 + ------------------------------- +*/ + +#include +#if defined(ESP8266) + /* ESP8266 Dependencies */ + #include + #include + #include +#elif defined(ESP32) + /* ESP32 Dependencies */ + #include + #include + #include +#endif + +#include + +/* Your WiFi Credentials */ +const char* ssid = ""; // SSID +const char* password = ""; // Password + +/* Start Webserver */ +AsyncWebServer server(80); + +/* Attach ESP-DASH to AsyncWebServer */ +ESPDash dashboard(server, true); + +// Cards +dash::FeedbackCard feedback(dashboard, "Status", dash::Status::SUCCESS); +dash::GenericCard genericString(dashboard, "Generic String"); +dash::GenericCard genericFloat(dashboard, "Generic Float"); +dash::GenericCard genericInt(dashboard, "Generic Int"); +dash::HumidityCard hum(dashboard, "Humidity"); // set decimal precision is 3 +dash::ProgressCard progressFloat(dashboard, "Progress Float", 0, 1, "kWh"); +dash::ProgressCard progressInt(dashboard, "Progress Int", 0, 100, "%"); +dash::SliderCard sliderFloatP4(dashboard, "Float Slider (4)", 0, 1, 0.0001f); +dash::SliderCard sliderFloatP2(dashboard, "Float Slider (2)", 0, 1, 0.01f); +dash::SliderCard sliderInt(dashboard, "Int Slider", 0, 255, 1, "bits"); +dash::SliderCard updateDelay(dashboard, "Update Delay", 1000, 20000, 1000, "ms"); +dash::SwitchCard button(dashboard, "Button"); +dash::TemperatureCard temp(dashboard, "Temperature"); // default precision is 2 + +// Charts +dash::BarChart bar(dashboard, "Power Usage (kWh)"); + +// Custom Statistics +dash::StatisticValue stat1(dashboard, "Statistic 1"); +dash::StatisticValue stat2(dashboard, "Statistic 2"); +dash::StatisticProvider statProvider(dashboard, "Statistic Provider"); + +uint8_t test_status = 0; + +/** + * Note how we are keeping all the chart data in global scope. + */ +// Bar Chart Data +const char* BarXAxis[] = {"1/4/22", "2/4/22", "3/4/22", "4/4/22", "5/4/22", "6/4/22", "7/4/22", "8/4/22", "9/4/22", "10/4/22", "11/4/22", "12/4/22", "13/4/22", "14/4/22", "15/4/22", "16/4/22", "17/4/22", "18/4/22", "19/4/22", "20/4/22", "21/4/22", "22/4/22", "23/4/22", "24/4/22", "25/4/22", "26/4/22", "27/4/22", "28/4/22", "29/4/22", "30/4/22"}; +int BarYAxis[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +unsigned long last_update_millis = 0; +uint32_t update_delay = 2000; + +void setup() { + Serial.begin(115200); + Serial.println(); + /* Connect WiFi */ + + // WiFi.persistent(false); + // WiFi.mode(WIFI_STA); + // WiFi.begin(ssid, password); + // while (WiFi.status() != WL_CONNECTED) { + // delay(500); + // Serial.print("."); + // } + // Serial.print("IP Address: "); + // Serial.println(WiFi.localIP()); + + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); + + /* Attach Button Callback */ + button.onChange([&](bool state) { + /* Print our new button value received from dashboard */ + Serial.println(String("Button Triggered: ") + (state ? "true" : "false")); + /* Make sure we update our button's value and send update to dashboard */ + button.setValue(state); + dashboard.refresh(button); + }); + + // Set Slider Index + sliderInt.setIndex(1); + + /* Attach Slider Callback */ + sliderInt.onChange([&](int value) { + /* Print our new slider value received from dashboard */ + Serial.println("Slider Triggered: " + String(value)); + /* Make sure we update our slider's value and send update to dashboard */ + sliderInt.setValue(value); + dashboard.refresh(sliderInt); + }); + + sliderFloatP2.setIndex(2); + sliderFloatP2.onChange([&](float value) { + Serial.println("Slider Float P2 Triggered: " + String(value)); + sliderFloatP2.setValue(value); + dashboard.refresh(sliderFloatP2); + }); + + sliderFloatP4.setIndex(3); + sliderFloatP4.onChange([&](float value) { + Serial.println("Slider Float P4 Triggered: " + String(value, 4)); + sliderFloatP4.setValue(value); + dashboard.refresh(sliderFloatP4); + }); + + updateDelay.setValue(update_delay); + updateDelay.onChange([&](uint32_t value) { + update_delay = value; + updateDelay.setValue(value); + dashboard.refresh(updateDelay); + }); + + stat1.setValue("Value 1"); + stat2.setValue(10.0 / 3.0); + statProvider.setProvider([]() { return millis(); }); + + bar.setX(BarXAxis, 30); + + genericFloat.setValue(10.0 / 3.0); // default rounding is 2 + genericString.setValue("Hello World!"); + + /* Start AsyncWebServer */ + server.begin(); + + server.onNotFound([](AsyncWebServerRequest* request) { + request->send(404); + }); +} + +void loop() { + // Update Everything every 2 seconds using millis if connected to WiFi + if (millis() - last_update_millis > update_delay && dashboard.hasClient()) { + last_update_millis = millis(); + + // Randomize Bar Chart YAxis Values ( for demonstration purposes only ) + for (int i = 0; i < 30; i++) { + BarYAxis[i] = (int)random(0, 200); + } + + /* Update Chart Y Axis (yaxis_array, array_size) */ + bar.setY(BarYAxis, 30); + + // Update all cards with random values + genericInt.setSymbol(random(0, 2) ? "unit1" : "unit2"); + genericInt.setValue((int)random(0, 100)); + temp.setValue(random(0, 100) / 3.0); + hum.setValue(random(0, 100) / 3.0); + + progressInt.setValue(random(0, 200)); // if more than max, clamped to max + progressFloat.setValue(random(0, 1000) / 2000.0); // if more than max, clamped to max + + sliderInt.setValue(random(0, 200)); // clamped at 255 by max + sliderFloatP2.setValue(random(0, 100) / 333.0); + sliderFloatP4.setValue(random(0, 100) / 333.0); + + // Loop through statuses + if (test_status == 0) { + feedback.setFeedback("Success Msg!", dash::Status::SUCCESS); + test_status = 1; + } else if (test_status == 1) { + feedback.setFeedback("Warning Msg!", dash::Status::WARNING); + test_status = 2; + } else if (test_status == 2) { + feedback.setFeedback("Danger Msg!", dash::Status::DANGER); + test_status = 3; + } else if (test_status == 3) { + feedback.setFeedback("Idle Msg!", dash::Status::IDLE); + test_status = 0; + } + + if (random(0, 2)) + button.toggle(); + + // Get Free Heap + Serial.println("Free Heap (Before Update): " + String(ESP.getFreeHeap())); + dashboard.sendUpdates(); + Serial.println("Free Heap (After Update): " + String(ESP.getFreeHeap())); + } +} \ No newline at end of file diff --git a/library.json b/library.json index 2ae4065d..083e21bf 100644 --- a/library.json +++ b/library.json @@ -22,13 +22,13 @@ { "owner": "bblanchon", "name": "ArduinoJson", - "version": "^7.1.0", + "version": "^7.2.1", "platforms": ["espressif8266", "espressif32"] }, { "owner": "mathieucarbou", "name": "ESPAsyncWebServer", - "version": "^3.3.14", + "version": "^3.3.23", "platforms": ["espressif8266", "espressif32"] } ] diff --git a/platformio.ini b/platformio.ini index d57f0eae..e05ef580 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,8 @@ lib_dir = . ; src_dir = examples/AccessPoint ; src_dir = examples/Basic -src_dir = examples/Benchmark +; src_dir = examples/Benchmark +src_dir = examples/Benchmark5 ; src_dir = examples/Chart ; src_dir = examples/Dynamic ; src_dir = examples/Interactive @@ -10,12 +11,19 @@ src_dir = examples/Benchmark [env] framework = arduino build_flags = + -std=c++17 + -std=gnu++17 -Wall -Wextra -D CONFIG_ARDUHAL_LOG_COLORS -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + ; -D DASH_USE_LEGACY_CHART_STORAGE=1 + ; -D DASH_USE_STL_STRING=1 + ; -D DASH_DEBUG +build_unflags = + -std=gnu++11 lib_deps = - bblanchon/ArduinoJson@^7.1.0 - mathieucarbou/ESPAsyncWebServer@^3.3.14 + bblanchon/ArduinoJson@^7.2.1 + mathieucarbou/ESPAsyncWebServer@^3.3.23 upload_protocol = esptool monitor_speed = 115200 monitor_filters = esp32_exception_decoder, log2file @@ -26,7 +34,8 @@ board = esp32-s3-devkitc-1 [env:arduino-3] platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip -board = esp32-s3-devkitc-1 +; board = esp32-s3-devkitc-1 +board = esp32dev [env:esp8266] platform = espressif8266 diff --git a/src/Card.cpp b/src/Card.cpp deleted file mode 100644 index c890c944..00000000 --- a/src/Card.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "Card.h" - -/* - Constructor -*/ -Card::Card(ESPDash *dashboard, const int type, const char* name, const char* symbol, const int min, const int max, const int step){ - _dashboard = dashboard; - _id = dashboard->nextId(); - _type = type; - _name = name; - _symbol = symbol; - _value_min = min; - _value_max = max; - _value_step = step; - _dashboard->add(this); -} - -Card::Card(ESPDash *dashboard, const int type, const char* name, const char* symbol, const float min, const float max, const float step){ - _dashboard = dashboard; - _id = dashboard->nextId(); - _type = type; - _name = name; - _symbol = symbol; - _value_min_f = min; - _value_max_f = max; - _value_step_f = step; - - _value_type = Card::FLOAT; - - _dashboard->add(this); -} - -/* - Attach Function Callback -*/ -void Card::attachCallback(std::function cb){ - _callback = cb; -} - -/* - Attach Function Callback -*/ -void Card::attachCallbackF(std::function cb){ - _callback_f = cb; -} - -/* - Value update methods -*/ -void Card::update(int value, const char* symbol){ - /* Clear String if it was used before */ - if(_value_type == Card::STRING){ - _value_s = ""; - } - /* Store new value */ - _value_type = Card::INTEGER; - if(strcmp(_symbol.c_str(), symbol) != 0 || _value_i != value) - _changed = true; - _value_i = value; - _symbol = symbol; -} - -void Card::update(int value){ - /* Clear String if it was used before */ - if(_value_type == Card::STRING){ - _value_s = ""; - } - /* Store new value */ - _value_type = Card::INTEGER; - if(_value_i != value) - _changed = true; - _value_i = value; -} - -void Card::update(float value, const char* symbol){ - /* Clear String if it was used before */ - if(_value_type == Card::STRING){ - _value_s = ""; - } - /* Store new value */ - _value_type = Card::FLOAT; - if(strcmp(_symbol.c_str(), symbol) != 0 || _value_f != value) - _changed = true; - _value_f = value; - _symbol = symbol; -} - -void Card::update(float value){ - /* Clear String if it was used before */ - if(_value_type == Card::STRING){ - _value_s = ""; - } - /* Store new value */ - _value_type = Card::FLOAT; - if(_value_f != value) - _changed = true; - _value_f = value; -} - -void Card::update(const String &value, const char* symbol){ - update(value.c_str(), symbol); -} - -void Card::update(const String &value){ - update(value.c_str()); -} - -void Card::update(const char* value, const char* symbol){ - if(_value_type == Card::STRING){ - if(strcmp(_value_s.c_str(), value) != 0) - _changed = true; - } - if (strcmp(_symbol.c_str(), symbol) != 0) { - _changed = true; - } - _value_type = Card::STRING; - _symbol = symbol; - _value_s = value; -} - -void Card::update(const char* value){ - if(_value_type == Card::STRING){ - if(strcmp(_value_s.c_str(), value) != 0) - _changed = true; - } - - _value_type = Card::STRING; - _value_s = value; -} - -void Card::update(bool value, const char* symbol){ - /* Clear String if it was used before */ - if(_value_type == Card::STRING){ - _value_s = ""; - } - /* Store new value */ - _value_type = Card::INTEGER; - if(strcmp(_symbol.c_str(), symbol) != 0 || _value_i != value) - _changed = true; - _value_i = value; - _symbol = symbol; -} - -void Card::update(bool value){ - /* Clear String if it was used before */ - if(_value_type == Card::STRING){ - _value_s = ""; - } - /* Store new value */ - _value_type = Card::INTEGER; - if(_value_i != value) - _changed = true; - _value_i = value; -} - -/* - Destructor -*/ -Card::~Card(){ - _dashboard->remove(this); -} diff --git a/src/Card.h b/src/Card.h index 5e2948b1..592129d9 100644 --- a/src/Card.h +++ b/src/Card.h @@ -1,47 +1,34 @@ #ifndef __CARD_H #define __CARD_H +#include "dash/DashCards.h" -#include -#include "Arduino.h" +#define BUTTON_CARD dash::Component::Type::CARD_BUTTON +#define GENERIC_CARD dash::Component::Type::CARD_GENERIC +#define HUMIDITY_CARD dash::Component::Type::CARD_HUMIDITY +#define PROGRESS_CARD dash::Component::Type::CARD_PROGRESS +#define SLIDER_CARD dash::Component::Type::CARD_SLIDER +#define STATUS_CARD dash::Component::Type::CARD_STATUS +#define TEMPERATURE_CARD dash::Component::Type::CARD_TEMPERATURE -#include "ESPDash.h" -#include "ArduinoJson.h" - -struct CardNames { - int value; - const char* type; -}; - -// functions defaults to zero (number card) -enum { - GENERIC_CARD, - TEMPERATURE_CARD, - HUMIDITY_CARD, - STATUS_CARD, - SLIDER_CARD, - BUTTON_CARD, - PROGRESS_CARD -}; - -// Forward Declaration -class ESPDash; +#define DASH_STATUS_IDLE "i" +#define DASH_STATUS_SUCCESS "s" +#define DASH_STATUS_WARNING "w" +#define DASH_STATUS_DANGER "d" // Card Class -class Card { +class [[deprecated("This class is deprecated. Use a dash::Card sub-class instead.")]] Card : public dash::Widget { private: - ESPDash *_dashboard; + ESPDash* _dashboard = nullptr; - uint32_t _id; - const char* _name; - int _type; - bool _changed; - enum { INTEGER, FLOAT, STRING } _value_type; + enum { INTEGER, + FLOAT, + STRING } _value_type; union alignas(4) { float _value_f; int _value_i; }; - String _value_s; + dash::string _value_s; union alignas(4) { float _value_min_f; int _value_min; @@ -54,29 +41,25 @@ class Card { float _value_step_f; int _value_step; }; - String _symbol; + dash::string _symbol; + std::function _callback = nullptr; std::function _callback_f = nullptr; public: - Card(ESPDash *dashboard, const int type, const char* name, const char* symbol = "", const int min = 0, const int max = 0, const int step = 1); - Card(ESPDash *dashboard, const int type, const char* name, const char* symbol, const float min, const float max, const float step); - void attachCallback(std::function cb); - void attachCallbackF(std::function cb); - void update(int value); - void update(int value, const char* symbol); - void update(bool value); - void update(bool value, const char* symbol); - void update(float value); - void update(float value, const char* symbol); - void update(const char* value); - void update(const char* value, const char* symbol); - void update(const String &value); - void update(const String &value, const char* symbol); + Card(ESPDash* dashboard, const dash::Component::Type type, const char* name, const char* symbol = "", const int min = 0, const int max = 0, const int step = 1); + Card(ESPDash* dashboard, const dash::Component::Type type, const char* name, const char* symbol, const float min, const float max, const float step); + void attachCallback(std::function cb) { _callback = cb; } + void attachCallbackF(std::function cb) { _callback_f = cb; } + void update(int value, const char* symbol = nullptr); + void update(bool value, const char* symbol = nullptr); + void update(float value, const char* symbol = nullptr); + void update(const char* value, const char* symbol = nullptr); + void update(const dash::string& value, const char* symbol = nullptr) { update(value.c_str(), symbol); } + void update(dash::string&& value, const char* symbol = nullptr); + virtual void toJson(const JsonObject& json, bool onlyChanges) const override; + virtual void onEvent(const JsonObject& json) override; ~Card(); - - friend class ESPDash; }; - #endif diff --git a/src/Chart.cpp b/src/Chart.cpp deleted file mode 100644 index 7397a85c..00000000 --- a/src/Chart.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "Chart.h" - -/* - Constructor -*/ -Chart::Chart(ESPDash *dashboard, const int type, const char* name){ - _dashboard = dashboard; - _id = dashboard->nextId(); - _type = type; - _name = name; - _dashboard->add(this); -} - -#if DASH_USE_LEGACY_CHART_STORAGE == 1 - void Chart::emptyXAxisVectors() { - if(!_x_axis_i.Empty()) - _x_axis_i.Clear(); - if(!_x_axis_f.Empty()) - _x_axis_f.Clear(); - if(!_x_axis_s.Empty()) - _x_axis_s.Clear(); - } - - void Chart::emptyYAxisVectors() { - if(!_y_axis_i.Empty()) - _y_axis_i.Clear(); - if(!_y_axis_f.Empty()) - _y_axis_f.Clear(); - } -#else - void Chart::clearXAxisPointers() { - _x_axis_i_ptr = nullptr; - _x_axis_f_ptr = nullptr; - _x_axis_char_ptr = nullptr; - _x_axis_s_ptr = nullptr; - _x_axis_ptr_size = 0; - } - - void Chart::clearYAxisPointers() { - _y_axis_i_ptr = nullptr; - _y_axis_f_ptr = nullptr; - _y_axis_ptr_size = 0; - } -#endif - -/* - Value update methods -*/ -void Chart::updateX(int arr_x[], size_t x_size){ - _x_axis_type = GraphAxisType::INTEGER; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - emptyXAxisVectors(); - for(int i=0; i < x_size; i++){ - _x_axis_i.PushBack(arr_x[i]); - } - #else - clearXAxisPointers(); - _x_axis_i_ptr = arr_x; - _x_axis_ptr_size = x_size; - #endif - _x_changed = true; -} - -void Chart::updateX(float arr_x[], size_t x_size){ - _x_axis_type = GraphAxisType::FLOAT; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - emptyXAxisVectors(); - for(int i=0; i < x_size; i++){ - _x_axis_f.PushBack(arr_x[i]); - } - #else - clearXAxisPointers(); - _x_axis_f_ptr = arr_x; - _x_axis_ptr_size = x_size; - #endif - _x_changed = true; -} - -void Chart::updateX(String arr_x[], size_t x_size){ - _x_axis_type = GraphAxisType::STRING; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - emptyXAxisVectors(); - for(int i=0; i < x_size; i++){ - _x_axis_s.PushBack(arr_x[i].c_str()); - } - #else - clearXAxisPointers(); - _x_axis_s_ptr = arr_x; - _x_axis_ptr_size = x_size; - #endif - _x_changed = true; -} - -void Chart::updateX(const char* arr_x[], size_t x_size){ - _x_axis_type = GraphAxisType::CHAR; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - emptyXAxisVectors(); - for(int i=0; i < x_size; i++){ - _x_axis_s.PushBack(String(arr_x[i])); - } - #else - clearXAxisPointers(); - _x_axis_char_ptr = arr_x; - _x_axis_ptr_size = x_size; - #endif - _x_changed = true; -} - -void Chart::updateY(int arr_y[], size_t y_size){ - _y_axis_type = GraphAxisType::INTEGER; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - emptyYAxisVectors(); - for(int i=0; i < y_size; i++){ - _y_axis_i.PushBack(arr_y[i]); - } - #else - clearYAxisPointers(); - _y_axis_i_ptr = arr_y; - _y_axis_ptr_size = y_size; - #endif - _y_changed = true; -} - -void Chart::updateY(float arr_y[], size_t y_size){ - _y_axis_type = GraphAxisType::FLOAT; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - emptyYAxisVectors(); - for(int i=0; i < y_size; i++){ - _y_axis_f.PushBack(arr_y[i]); - } - #else - clearYAxisPointers(); - _y_axis_f_ptr = arr_y; - _y_axis_ptr_size = y_size; - #endif - _y_changed = true; -} - -/* - Destructor -*/ -Chart::~Chart(){ - _dashboard->remove(this); -} \ No newline at end of file diff --git a/src/Chart.h b/src/Chart.h index e4bfb912..22ba1425 100644 --- a/src/Chart.h +++ b/src/Chart.h @@ -1,83 +1,67 @@ #ifndef __CHART_H #define __CHART_H -#include -#include "Arduino.h" -#include "vector.h" - -#include "ESPDash.h" -#include "ArduinoJson.h" +#include "dash/DashWidget.h" #ifndef DASH_USE_LEGACY_CHART_STORAGE #define DASH_USE_LEGACY_CHART_STORAGE 0 #endif -// Default to Line Chart -enum { - BAR_CHART, -}; - -struct ChartNames { - int value; - const char* type; -}; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + #include +#endif -enum GraphAxisType { INTEGER, FLOAT, CHAR, STRING }; +enum GraphAxisType { INTEGER, + FLOAT, + CHAR, + STRING }; -// Forward Declaration -class ESPDash; +#define BAR_CHART dash::Component::Type::CHART_BAR // Chart Class -class Chart { +class [[deprecated("This class is deprecated. Use a dash::Chart sub-class instead.")]] Chart : public dash::Widget { private: - ESPDash *_dashboard; - - uint32_t _id; - const char *_name; - int _type; - bool _x_changed; - bool _y_changed; + ESPDash* _dashboard = nullptr; GraphAxisType _x_axis_type; GraphAxisType _y_axis_type; - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - /* X-Axis */ - Vector _x_axis_i; - Vector _x_axis_f; - Vector _x_axis_s; - /* Y-Axis */ - Vector _y_axis_i; - Vector _y_axis_f; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + /* X-Axis */ + std::vector _x_axis_i; + std::vector _x_axis_f; + std::vector _x_axis_s; + /* Y-Axis */ + std::vector _y_axis_i; + std::vector _y_axis_f; - void emptyXAxisVectors(); - void emptyYAxisVectors(); - #else - /* X-Axis */ - int *_x_axis_i_ptr = nullptr; - float *_x_axis_f_ptr = nullptr; - const char **_x_axis_char_ptr = nullptr; - String *_x_axis_s_ptr = nullptr; - unsigned int _x_axis_ptr_size = 0; - /* Y-Axis */ - int *_y_axis_i_ptr = nullptr; - float *_y_axis_f_ptr = nullptr; - unsigned int _y_axis_ptr_size = 0; + void emptyXAxisVectors(); + void emptyYAxisVectors(); +#else + /* X-Axis */ + int* _x_axis_i_ptr = nullptr; + float* _x_axis_f_ptr = nullptr; + const char** _x_axis_char_ptr = nullptr; + dash::string* _x_axis_s_ptr = nullptr; + unsigned int _x_axis_ptr_size = 0; + /* Y-Axis */ + int* _y_axis_i_ptr = nullptr; + float* _y_axis_f_ptr = nullptr; + unsigned int _y_axis_ptr_size = 0; - void clearXAxisPointers(); - void clearYAxisPointers(); - #endif + void clearXAxisPointers(); + void clearYAxisPointers(); +#endif public: - Chart(ESPDash *dashboard, const int type, const char* name); + Chart(ESPDash* dashboard, const dash::Component::Type type, const char* name) : dash::Widget(*dashboard, name, type), _dashboard(dashboard) {} void updateX(int arr_x[], size_t x_size); void updateX(float arr_x[], size_t x_size); - void updateX(String arr_x[], size_t x_size); + void updateX(dash::string arr_x[], size_t x_size); void updateX(const char* arr_x[], size_t x_size); void updateY(int arr_y[], size_t y_size); void updateY(float arr_y[], size_t y_size); + virtual void toJson(const JsonObject& json, bool onlyChanges) const override; ~Chart(); - - friend class ESPDash; }; #endif \ No newline at end of file diff --git a/src/DashDeprecations.cpp b/src/DashDeprecations.cpp new file mode 100644 index 00000000..e9e53c35 --- /dev/null +++ b/src/DashDeprecations.cpp @@ -0,0 +1,385 @@ +// This file holds all the deprecated class and method implementations + +#include "Card.h" +#include "Chart.h" +#include "Statistic.h" + +#include "ESPDash.h" + +Statistic::~Statistic() { _dashboard->remove(*this); } + +/* + Chart +*/ + +#if DASH_USE_LEGACY_CHART_STORAGE == 1 +void Chart::emptyXAxisVectors() { + if (!_x_axis_i.empty()) + _x_axis_i.clear(); + if (!_x_axis_f.empty()) + _x_axis_f.clear(); + if (!_x_axis_s.empty()) + _x_axis_s.clear(); +} + +void Chart::emptyYAxisVectors() { + if (!_y_axis_i.empty()) + _y_axis_i.clear(); + if (!_y_axis_f.empty()) + _y_axis_f.clear(); +} +#else +void Chart::clearXAxisPointers() { + _x_axis_i_ptr = nullptr; + _x_axis_f_ptr = nullptr; + _x_axis_char_ptr = nullptr; + _x_axis_s_ptr = nullptr; + _x_axis_ptr_size = 0; +} + +void Chart::clearYAxisPointers() { + _y_axis_i_ptr = nullptr; + _y_axis_f_ptr = nullptr; + _y_axis_ptr_size = 0; +} +#endif + +void Chart::updateX(int arr_x[], size_t x_size) { + _x_axis_type = GraphAxisType::INTEGER; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + emptyXAxisVectors(); + for (int i = 0; i < x_size; i++) { + _x_axis_i.push_back(arr_x[i]); + } +#else + clearXAxisPointers(); + _x_axis_i_ptr = arr_x; + _x_axis_ptr_size = x_size; +#endif + setChange(Property::AXIS_X); +} + +void Chart::updateX(float arr_x[], size_t x_size) { + _x_axis_type = GraphAxisType::FLOAT; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + emptyXAxisVectors(); + for (int i = 0; i < x_size; i++) { + _x_axis_f.push_back(arr_x[i]); + } +#else + clearXAxisPointers(); + _x_axis_f_ptr = arr_x; + _x_axis_ptr_size = x_size; +#endif + setChange(Property::AXIS_X); +} + +void Chart::updateX(dash::string arr_x[], size_t x_size) { + _x_axis_type = GraphAxisType::STRING; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + emptyXAxisVectors(); + for (int i = 0; i < x_size; i++) { + _x_axis_s.push_back(arr_x[i].c_str()); + } +#else + clearXAxisPointers(); + _x_axis_s_ptr = arr_x; + _x_axis_ptr_size = x_size; +#endif + setChange(Property::AXIS_X); +} + +void Chart::updateX(const char* arr_x[], size_t x_size) { + _x_axis_type = GraphAxisType::CHAR; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + emptyXAxisVectors(); + for (int i = 0; i < x_size; i++) { + _x_axis_s.push_back(arr_x[i]); + } +#else + clearXAxisPointers(); + _x_axis_char_ptr = arr_x; + _x_axis_ptr_size = x_size; +#endif + setChange(Property::AXIS_X); +} + +void Chart::updateY(int arr_y[], size_t y_size) { + _y_axis_type = GraphAxisType::INTEGER; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + emptyYAxisVectors(); + for (int i = 0; i < y_size; i++) { + _y_axis_i.push_back(arr_y[i]); + } +#else + clearYAxisPointers(); + _y_axis_i_ptr = arr_y; + _y_axis_ptr_size = y_size; +#endif + setChange(Property::AXIS_Y); +} + +void Chart::updateY(float arr_y[], size_t y_size) { + _y_axis_type = GraphAxisType::FLOAT; +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + emptyYAxisVectors(); + for (int i = 0; i < y_size; i++) { + _y_axis_f.push_back(arr_y[i]); + } +#else + clearYAxisPointers(); + _y_axis_f_ptr = arr_y; + _y_axis_ptr_size = y_size; +#endif + setChange(Property::AXIS_Y); +} + +void Chart::toJson(const JsonObject& json, bool onlyChanges) const { + dash::Widget::toJson(json, onlyChanges); + + if (!onlyChanges || hasChanged(Property::AXIS_X)) { + JsonArray xAxis = json["x"].to(); + switch (_x_axis_type) { + case GraphAxisType::INTEGER: +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + for (int i = 0; i < _x_axis_i.size(); i++) + xAxis.add(_x_axis_i[i]); +#else + if (_x_axis_i_ptr != nullptr) { + for (unsigned int i = 0; i < _x_axis_ptr_size; i++) + xAxis.add(_x_axis_i_ptr[i]); + } +#endif + break; + case GraphAxisType::FLOAT: +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + for (int i = 0; i < _x_axis_f.size(); i++) + xAxis.add(_x_axis_f[i]); +#else + if (_x_axis_f_ptr != nullptr) { + for (unsigned int i = 0; i < _x_axis_ptr_size; i++) + xAxis.add(_x_axis_f_ptr[i]); + } +#endif + break; + case GraphAxisType::CHAR: +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + for (int i = 0; i < _x_axis_s.size(); i++) + xAxis.add(_x_axis_s[i].c_str()); +#else + if (_x_axis_char_ptr != nullptr) { + for (unsigned int i = 0; i < _x_axis_ptr_size; i++) + xAxis.add(_x_axis_char_ptr[i]); + } +#endif + break; + case GraphAxisType::STRING: +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + for (int i = 0; i < _x_axis_s.size(); i++) + xAxis.add(_x_axis_s[i].c_str()); +#else + if (_x_axis_s_ptr != nullptr) { + for (unsigned int i = 0; i < _x_axis_ptr_size; i++) + xAxis.add(_x_axis_s_ptr[i]); + } +#endif + break; + default: + // blank value + break; + } + } + + if (!onlyChanges || hasChanged(Property::AXIS_Y)) { + JsonArray yAxis = json["y"].to(); + switch (_y_axis_type) { + case GraphAxisType::INTEGER: +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + for (int i = 0; i < _y_axis_i.size(); i++) + yAxis.add(_y_axis_i[i]); +#else + if (_y_axis_i_ptr != nullptr) { + for (unsigned int i = 0; i < _y_axis_ptr_size; i++) + yAxis.add(_y_axis_i_ptr[i]); + } +#endif + break; + case GraphAxisType::FLOAT: +#if DASH_USE_LEGACY_CHART_STORAGE == 1 + for (int i = 0; i < _y_axis_f.size(); i++) + yAxis.add(_y_axis_f[i]); +#else + if (_y_axis_f_ptr != nullptr) { + for (unsigned int i = 0; i < _y_axis_ptr_size; i++) + yAxis.add(_y_axis_f_ptr[i]); + } +#endif + break; + default: + // blank value + break; + } + } +} + +Chart::~Chart() { + _dashboard->remove(*this); +} + +/* + Card +*/ + +Card::Card(ESPDash* dashboard, const dash::Component::Type type, const char* name, const char* symbol, const int min, const int max, const int step) : dash::Widget(*dashboard, name, type), _dashboard(dashboard) { + _symbol = symbol ? symbol : ""; + _value_min = min; + _value_max = max; + _value_step = step; + setIndex(255); + _value_type = Card::INTEGER; +} + +Card::Card(ESPDash* dashboard, const dash::Component::Type type, const char* name, const char* symbol, const float min, const float max, const float step) : dash::Widget(*dashboard, name, type), _dashboard(dashboard) { + _symbol = symbol ? symbol : ""; + _value_min_f = min; + _value_max_f = max; + _value_step_f = step; + setIndex(255); + _value_type = Card::FLOAT; +} + +void Card::update(int value, const char* symbol) { + if (_value_type == Card::STRING) { + _value_s = ""; + } + _value_type = Card::INTEGER; + if (_value_i != value) { + _value_i = value; + setChange(Property::VALUE); + } + if (symbol && _symbol != symbol) { + _symbol = symbol; + setChange(Property::SYMBOL); + } +} + +void Card::update(float value, const char* symbol) { + if (_value_type == Card::STRING) { + _value_s = ""; + } + _value_type = Card::FLOAT; + if (_value_f != value) { + _value_f = value; + setChange(Property::VALUE); + } + if (symbol && _symbol != symbol) { + _symbol = symbol; + setChange(Property::SYMBOL); + } +} + +void Card::update(const char* value, const char* symbol) { + if (_value_type == Card::STRING) { + if (_value_s != value) { + _value_s = value; + setChange(Property::VALUE); + } + } + _value_type = Card::STRING; + if (symbol && _symbol != symbol) { + _symbol = symbol; + setChange(Property::SYMBOL); + } +} + +void Card::update(dash::string&& value, const char* symbol) { + if (_value_type == Card::STRING) { + if (_value_s != value) + _value_s = std::move(value); + setChange(Property::VALUE); + } + _value_type = Card::STRING; + if (symbol && _symbol != symbol) { + _symbol = symbol; + setChange(Property::SYMBOL); + } +} + +void Card::update(bool value, const char* symbol) { + if (_value_type == Card::STRING) { + _value_s = ""; + } + _value_type = Card::INTEGER; + if (_value_i != value) { + _value_i = value; + setChange(Property::VALUE); + } + if (symbol && _symbol != symbol) { + _symbol = symbol; + setChange(Property::SYMBOL); + } +} + +void Card::toJson(const JsonObject& json, bool onlyChanges) const { + dash::Widget::toJson(json, onlyChanges); + + if (!onlyChanges) { + // Don't add useless values to cards which don't require them + dash::Component::Type type = dash::Widget::type(); + if (type == SLIDER_CARD || type == PROGRESS_CARD) { + if (_value_type == Card::FLOAT) { + json["min"] = String(_value_min_f, 2); + json["max"] = String(_value_max_f, 2); + json["step"] = String(_value_step_f, 2); + } else { + json["min"] = _value_min; + json["max"] = _value_max; + if (_value_step != 1) + json["step"] = _value_step; + } + } + } + + if (!onlyChanges || hasChanged(Property::SYMBOL)) + json["s"] = _symbol; + + if (!onlyChanges || hasChanged(Property::VALUE)) { + switch (_value_type) { + case Card::INTEGER: + json["v"] = _value_i; + break; + case Card::FLOAT: + json["v"] = String(_value_f, 2); + break; + case Card::STRING: + if (_value_s.length()) { + json["v"] = _value_s; + } + break; + default: + // blank value + break; + } + } +} + +void Card::onEvent(const JsonObject& json) { + if (_callback && json["command"] == "button:clicked") { + _callback(json["value"].as()); + return; + } + + if (_callback_f && _value_type == Card::FLOAT && json["command"] == "slider:changed") { + _callback_f(json["value"].as()); + return; + } + + if (_callback && json["command"] == "slider:changed") { + _callback(json["value"].as()); + return; + } +} + +Card::~Card() { + _dashboard->remove(*this); +} diff --git a/src/ESPDash.cpp b/src/ESPDash.cpp index a0d34586..efd35ffa 100644 --- a/src/ESPDash.cpp +++ b/src/ESPDash.cpp @@ -1,53 +1,72 @@ #include "ESPDash.h" -// Integral type to string pairs events -// ID, type -struct CardNames cardTags[] = { - {GENERIC_CARD, "generic"}, - {TEMPERATURE_CARD, "temperature"}, - {HUMIDITY_CARD, "humidity"}, - {STATUS_CARD, "status"}, - {SLIDER_CARD, "slider"}, - {BUTTON_CARD, "button"}, - {PROGRESS_CARD, "progress"}, -}; - -// Integral type to string pairs events -// ID, type -struct ChartNames chartTags[] = { - {BAR_CHART, "bar"}, -}; +dash::Component::Component(ESPDash& dashboard, const char* name, Type type) : _id(nextId()), _name(name), _type(type) { + dashboard.add(*this); +} +ESPDash::ESPDash(AsyncWebServer& server, const char* uri, bool enable_default_stats) { + _server = &server; -/* - Constructors -*/ -ESPDash::ESPDash(AsyncWebServer* server) : ESPDash(server, "/", true) {} + if (enable_default_stats) { + // Hardware + dash::StatisticValue* hardware = new dash::StatisticValue("Hardware"); + hardware->setValue(DASH_HARDWARE); + _components.push_back(hardware); + _componentsOwned.push_back(hardware); + + // SDK Version + dash::StatisticValue* sdk = new dash::StatisticValue("SDK Version"); +#if defined(ESP8266) + sdk->setValue(ESP.getCoreVersion().c_str()); +#elif defined(ESP32) + sdk->setValue(esp_get_idf_version()); +#endif + _components.push_back(sdk); + _componentsOwned.push_back(sdk); + + // MAC Address + dash::StatisticValue* mac = new dash::StatisticValue("MAC Address"); + mac->setValue(WiFi.macAddress().c_str()); + _components.push_back(mac); + _componentsOwned.push_back(mac); -ESPDash::ESPDash(AsyncWebServer* server, bool enable_default_stats) : ESPDash(server, "/", enable_default_stats) {} + // Free Heap + dash::StatisticProvider* heap = new dash::StatisticProvider("Free Heap (SRAM)"); + heap->setProvider([]() { return ESP.getFreeHeap(); }); + _components.push_back(heap); + _componentsOwned.push_back(heap); + + // WiFi Mode + dash::StatisticProvider* wifi = new dash::StatisticProvider("WiFi Mode"); + wifi->setProvider([]() { return WiFi.getMode(); }); + _components.push_back(wifi); + _componentsOwned.push_back(wifi); -ESPDash::ESPDash(AsyncWebServer* server, const char* uri, bool enable_default_stats) { - _server = server; - default_stats_enabled = enable_default_stats; + // WiFi Signal + dash::StatisticProvider* signal = new dash::StatisticProvider("WiFi Signal"); + signal->setProvider([]() { return WiFi.RSSI(); }); + _components.push_back(signal); + _componentsOwned.push_back(signal); + } // Initialize AsyncWebSocket _ws = new AsyncWebSocket("/dashws"); // Attach AsyncWebServer Routes - _server->on(uri, HTTP_GET, [this](AsyncWebServerRequest *request){ - if(basic_auth){ - if(!request->authenticate(username.c_str(), password.c_str())) - return request->requestAuthentication(); + _server->on(uri, HTTP_GET, [this](AsyncWebServerRequest* request) { + if (basic_auth) { + if (!request->authenticate(username.c_str(), password.c_str())) + return request->requestAuthentication(); } // respond with the compressed frontend - AsyncWebServerResponse *response = request->beginResponse(200, "text/html", DASH_HTML, sizeof(DASH_HTML)); + AsyncWebServerResponse* response = request->beginResponse(200, "text/html", DASH_HTML, sizeof(DASH_HTML)); response->addHeader("Content-Encoding", "gzip"); response->addHeader("Cache-Control", "public, max-age=900"); request->send(response); }); // Websocket Callback Handler - _ws->onEvent([&](__unused AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len){ + _ws->onEvent([&](__unused AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { // Request Buffer #if ARDUINOJSON_VERSION_MAJOR == 7 JsonDocument json; @@ -56,9 +75,9 @@ ESPDash::ESPDash(AsyncWebServer* server, const char* uri, bool enable_default_st #endif if (type == WS_EVT_DATA) { - AwsFrameInfo * info = (AwsFrameInfo * ) arg; - if (info -> final && info -> index == 0 && info -> len == len) { - if (info -> opcode == WS_TEXT) { + AwsFrameInfo* info = (AwsFrameInfo*)arg; + if (info->final && info->index == 0 && info->len == len) { + if (info->opcode == WS_TEXT) { data[len] = 0; deserializeJson(json, reinterpret_cast(data)); // client side commands parsing @@ -66,38 +85,24 @@ ESPDash::ESPDash(AsyncWebServer* server, const char* uri, bool enable_default_st _asyncAccessInProgress = true; if (_beforeUpdateCallback) _beforeUpdateCallback(false); - generateLayoutJSON(client, false); + generateLayoutJSON(client, false, nullptr); _asyncAccessInProgress = false; } else if (json["command"] == "ping") { _ws->text(client->id(), "{\"command\":\"pong\"}"); - } else if (json["command"] == "button:clicked") { - // execute and reference card data struct to funtion - uint32_t id = json["id"].as(); - for(int i=0; i < cards.Size(); i++){ - Card *p = cards[i]; - if(id == p->_id){ - if(p->_callback != nullptr){ - _asyncAccessInProgress = true; - p->_callback(json["value"].as()); - _asyncAccessInProgress = false; - } - } - } - } else if (json["command"] == "slider:changed") { - // execute and reference card data struct to funtion - uint32_t id = json["id"].as(); - for(int i=0; i < cards.Size(); i++){ - Card *p = cards[i]; - if(id == p->_id){ - if(p->_callback_f != nullptr && p->_value_type == Card::FLOAT){ - _asyncAccessInProgress = true; - p->_callback_f(json["value"].as()); - _asyncAccessInProgress = false; - } else if(p->_callback != nullptr) { - _asyncAccessInProgress = true; - p->_callback(json["value"].as()); - _asyncAccessInProgress = false; - } + } else if (json["id"].is()) { + uint16_t id = json["id"].as(); + // find component with same id + for (auto c : _components) { + if (c->id() == id) { + _asyncAccessInProgress = true; +#ifdef DASH_DEBUG + dash::string jsonEvent; + serializeJson(json, jsonEvent); + DASH_LOGD("ESPDash", "[%d] %s: onEvent(%s): %s", c->id(), c->name(), json["command"].as(), jsonEvent.c_str()); +#endif + c->onEvent(json.as()); + _asyncAccessInProgress = false; + break; } } } @@ -110,244 +115,151 @@ ESPDash::ESPDash(AsyncWebServer* server, const char* uri, bool enable_default_st _server->addHandler(_ws); } -void ESPDash::setAuthentication(const char *user, const char *pass) { +void ESPDash::setAuthentication(const char* user, const char* pass) { username = user; password = pass; basic_auth = username.length() && password.length(); - if(basic_auth) { + if (basic_auth) { _ws->setAuthentication(username.c_str(), password.c_str()); } } -void ESPDash::setAuthentication(const String &user, const String &pass) { - setAuthentication(user.c_str(), pass.c_str()); -} - -// Add Card -void ESPDash::add(Card *card) { - cards.PushBack(card); -} - -// Remove Card -void ESPDash::remove(Card *card) { - for(int i=0; i < cards.Size(); i++){ - Card *p = cards[i]; - if(p->_id == card->_id){ - cards.Erase(i); - return; - } - } -} - - -// Add Chart -void ESPDash::add(Chart *chart) { - charts.PushBack(chart); -} - -// Remove Card -void ESPDash::remove(Chart *chart) { - for(int i=0; i < charts.Size(); i++){ - Chart *p = charts[i]; - if(p->_id == chart->_id){ - charts.Erase(i); - return; +// Add a component to the dashboard and return true if the component was added, false if the component ID was already present +bool ESPDash::add(dash::Component& component) { + for (auto c : _components) + if (c->id() == component.id()) { +#ifdef DASH_DEBUG + DASH_LOGW("ESPDash", "Component with ID %" PRIu32 " already exists", component.id()); +#endif + return false; } - } -} - -// Add Statistic -void ESPDash::add(Statistic *statistic) { - statistics.PushBack(statistic); + _components.push_back(&component); + return true; } -// Remove Statistic -void ESPDash::remove(Statistic *statistic) { - for(int i=0; i < statistics.Size(); i++){ - Statistic *p = statistics[i]; - if(p->_id == statistic->_id){ - statistics.Erase(i); - return; - } - } +void ESPDash::remove(dash::Component& component) { + _components.remove(&component); } // generates the layout JSON string to the frontend -void ESPDash::generateLayoutJSON(AsyncWebSocketClient* client, bool changes_only, Card* onlyCard, Chart* onlyChart) { +void ESPDash::generateLayoutJSON(AsyncWebSocketClient* client, bool changes_only, const dash::Component* onlyComponent) { +#ifdef DASH_DEBUG + DASH_LOGD("ESPDash", "generateLayoutJSON(%p, %d, %p)", client, changes_only, onlyComponent); +#endif + #if ARDUINOJSON_VERSION_MAJOR == 6 DynamicJsonDocument doc(DASH_JSON_DOCUMENT_ALLOCATION); #else JsonDocument doc; #endif - // preparing layout - if (!changes_only) { - doc["command"] = "update:layout:begin"; - } else { + if (changes_only || onlyComponent) { + // send only the components that have changed or a specific component doc["command"] = "update:components"; - } - if (!changes_only) { - send(client, doc); - } - // Generate JSON for all Cards - doc["command"] = changes_only ? "update:components" : "update:layout:next"; - for (int i = 0; i < cards.Size(); i++) { - Card* c = cards[i]; - if (changes_only) { - if (!c->_changed && (onlyCard == nullptr || onlyCard->_id != c->_id)) { - continue; - } + if (onlyComponent) { + if (generateLayoutJSON(client, true, onlyComponent, doc, onlyComponent->family())) + send(client, doc); + + } else { + // when sending updates, go through all components in order of family + // and try to pack as many updates as possible in the same payload + size_t docSize = 0; + docSize += generateLayoutJSON(client, true, onlyComponent, doc, dash::Component::Family::STATISTIC); + docSize += generateLayoutJSON(client, true, onlyComponent, doc, dash::Component::Family::CARD); + docSize += generateLayoutJSON(client, true, onlyComponent, doc, dash::Component::Family::CHART); + if (docSize > 0) + send(client, doc); } - // Generate JSON -#if ARDUINOJSON_VERSION_MAJOR == 6 - JsonObject obj = doc["cards"].createNestedObject(); -#else - JsonObject obj = doc["cards"].add(); -#endif - generateComponentJSON(obj, c, changes_only); + } else { + doc["command"] = "update:layout:begin"; + send(client, doc); - if (overflowed(doc)) { - doc["cards"].as().remove(doc["cards"].as().size() - 1); + if (generateLayoutJSON(client, false, nullptr, doc, dash::Component::Family::STATISTIC)) send(client, doc); - doc["command"] = changes_only ? "update:components" : "update:layout:next"; - i--; - continue; - } - // Clear change flags - if (changes_only) { - c->_changed = false; - } - } + if (generateLayoutJSON(client, false, nullptr, doc, dash::Component::Family::CARD)) + send(client, doc); - if (doc["cards"].as().size() > 0) - send(client, doc); + if (generateLayoutJSON(client, false, nullptr, doc, dash::Component::Family::CHART)) + send(client, doc); + } +} - // Generate JSON for all Charts +size_t ESPDash::generateLayoutJSON(AsyncWebSocketClient* client, bool changes_only, const dash::Component* onlyComponent, JsonDocument& doc, dash::Component::Family family) { + size_t docSize = 0; doc["command"] = changes_only ? "update:components" : "update:layout:next"; - for (int i = 0; i < charts.Size(); i++) { - Chart* c = charts[i]; - if (changes_only) { - if (!c->_x_changed && !c->_y_changed && (onlyChart == nullptr || onlyChart->_id != c->_id)) { - continue; - } - } - // Generate JSON -#if ARDUINOJSON_VERSION_MAJOR == 7 - JsonObject obj = doc["charts"].add(); -#else - JsonObject obj = doc["charts"].createNestedObject(); -#endif - generateComponentJSON(obj, c, changes_only); + for (auto c : _components) { + // skip if onlyComponent is set and it is not the current component + if (onlyComponent && onlyComponent != c) + continue; - if (overflowed(doc)) { - doc["charts"].as().remove(doc["charts"].as().size() - 1); - send(client, doc); - doc["command"] = changes_only ? "update:components" : "update:layout:next"; - i--; + const dash::Component::Family f = c->family(); + if (f != family) continue; - } - // Clear change flags - if (changes_only) { - c->_x_changed = false; - c->_y_changed = false; - } - } + // for auto-updatable components like statistics provider + c->selfUpdate(); - if (doc["charts"].as().size() > 0) - send(client, doc); + // skip if we only want to send changes and the component has not changed + if (changes_only && !c->hasChanged()) + continue; - // Generate JSON for all Statistics - doc["command"] = changes_only ? "update:components" : "update:layout:next"; - int idx = 0; - - // Check if default statistics are needed - if (default_stats_enabled) { - if (!changes_only) { - // Hardware - doc["stats"][idx]["i"] = -1; - doc["stats"][idx]["k"] = "Hardware"; - doc["stats"][idx]["v"] = DASH_HARDWARE; - idx++; - - // SDK Version - doc["stats"][idx]["i"] = -2; - doc["stats"][idx]["k"] = "SDK Version"; -#if defined(ESP8266) - doc["stats"][idx]["v"] = ESP.getCoreVersion(); -#elif defined(ESP32) - doc["stats"][idx]["v"] = String(esp_get_idf_version()); -#endif - idx++; + const char* key = jsonKey(f); - // MAC Address - doc["stats"][idx]["i"] = -3; - doc["stats"][idx]["k"] = "MAC Address"; - doc["stats"][idx]["v"] = WiFi.macAddress(); - idx++; - } +#ifdef DASH_DEBUG + DASH_LOGD("ESPDash", "Generate %s/%d: %s", key, c->id(), c->name()); +#endif - // Free Heap - doc["stats"][idx]["i"] = -4; - doc["stats"][idx]["k"] = "Free Heap (SRAM)"; - doc["stats"][idx]["v"] = ESP.getFreeHeap(); - idx++; + JsonObject obj = doc[key].add(); + c->toJson(obj, changes_only); - // WiFi Mode - doc["stats"][idx]["i"] = -5; - doc["stats"][idx]["k"] = "WiFi Mode"; - doc["stats"][idx]["v"] = WiFi.getMode(); - idx++; + // check if json doc is full + if (doc.overflowed()) { + DASH_LOGW("ESPDash", "Doc overflow!"); - // WiFi Signal - doc["stats"][idx]["i"] = -6; - doc["stats"][idx]["k"] = "WiFi Signal"; - doc["stats"][idx]["v"] = WiFi.RSSI(); - idx++; - } + // send current data if json doc is full + send(client, doc); + docSize = 0; + doc["command"] = changes_only ? "update:components" : "update:layout:next"; - // Loop through user defined stats - for (int i = 0; i < statistics.Size(); i++) { - Statistic* s = statistics[i]; - if (changes_only) { - if (!s->_changed) { - continue; - } + // add the component back again since it was not added + obj = doc[key].add(); + c->toJson(obj, changes_only); } - doc["stats"][idx]["i"] = s->_id; - doc["stats"][idx]["k"] = s->_key; - if (changes_only || s->_value.length() > 0) - doc["stats"][idx]["v"] = s->_value; - doc["stats"][idx]["v"] = s->_value; - idx++; + docSize += measureJson(obj); - if (overflowed(doc)) { - doc["stats"].as().remove(idx - 1); + // check if we are above the payload size + if (docSize > DASH_JSON_SIZE) { +#ifdef DASH_DEBUG + DASH_LOGD("ESPDash", "Reached payload size: %u", docSize); +#endif + // send current data if we are above the payload size send(client, doc); + docSize = 0; doc["command"] = changes_only ? "update:components" : "update:layout:next"; - i--; - idx = 0; - continue; } - // Clear change flags - if (changes_only) { - s->_changed = false; - } + // component processed + if (changes_only) + c->clearChanges(); } - if (idx > 0) - send(client, doc); + return docSize; } void ESPDash::send(AsyncWebSocketClient* client, JsonDocument& doc) { const size_t len = measureJson(doc); - // ESP_LOGW("ESPDash", "Required Heap size to build WebSocket message: %d bytes. Free Heap: %" PRIu32 " bytes", len, ESP.getFreeHeap()); +#ifdef DASH_DEBUG + DASH_LOGD("ESPDash", "send(%u) - Free heap: %" PRIu32, len, ESP.getFreeHeap()); + // uncommenting this will print the JSON to the serial console but can be very verbose when having many components + // and can cause the websocket client in browser to timeout + // serializeJson(doc, Serial); + // Serial.println(); +#endif AsyncWebSocketMessageBuffer* buffer = _ws->makeBuffer(len); assert(buffer); serializeJson(doc, buffer->get(), len); @@ -359,150 +271,17 @@ void ESPDash::send(AsyncWebSocketClient* client, JsonDocument& doc) { doc.clear(); } -bool ESPDash::overflowed(JsonDocument& doc) { -#if DASH_JSON_SIZE > 0 // ArduinoJson 6 (mandatory) or 7 - return doc.overflowed() || measureJson(doc.as()) > DASH_JSON_SIZE; -#elif DASH_MIN_FREE_HEAP > 0 // ArduinoJson 7 only - return ESP.getFreeHeap() >= DASH_MIN_FREE_HEAP; -#else // ArduinoJson 7 only - return doc.overflowed(); -#endif -} - -/* - Generate Card JSON -*/ -void ESPDash::generateComponentJSON(JsonObject& doc, Card* card, bool change_only){ - doc["id"] = card->_id; - if (!change_only){ - doc["n"] = card->_name; - doc["t"] = cardTags[card->_type].type; - if (card->_type == SLIDER_CARD || card->_type == PROGRESS_CARD) { - if(card->_value_type == Card::FLOAT) { - doc["min"] = String(card->_value_min_f, 2); - doc["max"] = String(card->_value_max_f, 2); - doc["step"] = String(card->_value_step_f, 2); - } else { - doc["min"] = card->_value_min; - doc["max"] = card->_value_max; - doc["step"] = card->_value_step; - } - } - } - if(change_only || !card->_symbol.isEmpty()) - doc["s"] = card->_symbol; - - switch (card->_value_type) { - case Card::INTEGER: - doc["v"] = card->_value_i; - break; - case Card::FLOAT: - doc["v"] = String(card->_value_f, 2); - break; - case Card::STRING: - if(change_only || !card->_value_s.isEmpty()) { - doc["v"] = card->_value_s; - } - break; +const char* ESPDash::jsonKey(dash::Component::Family family) { + switch (family) { + case dash::Component::Family::CARD: + return "cards"; + case dash::Component::Family::CHART: + return "charts"; + case dash::Component::Family::STATISTIC: + return "stats"; default: - // blank value - break; - } -} - - -/* - Generate Chart JSON -*/ -void ESPDash::generateComponentJSON(JsonObject& doc, Chart* chart, bool change_only){ - doc["id"] = chart->_id; - if(!change_only){ - doc["n"] = chart->_name; - doc["t"] = chartTags[chart->_type].type; - } - - if(!change_only || chart->_x_changed) { - JsonArray xAxis = doc["x"].to(); - switch (chart->_x_axis_type) { - case GraphAxisType::INTEGER: - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - for(int i=0; i < chart->_x_axis_i.Size(); i++) - xAxis.add(chart->_x_axis_i[i]); - #else - if (chart->_x_axis_i_ptr != nullptr) { - for(unsigned int i=0; i < chart->_x_axis_ptr_size; i++) - xAxis.add(chart->_x_axis_i_ptr[i]); - } - #endif - break; - case GraphAxisType::FLOAT: - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - for(int i=0; i < chart->_x_axis_f.Size(); i++) - xAxis.add(chart->_x_axis_f[i]); - #else - if (chart->_x_axis_f_ptr != nullptr) { - for(unsigned int i=0; i < chart->_x_axis_ptr_size; i++) - xAxis.add(chart->_x_axis_f_ptr[i]); - } - #endif - break; - case GraphAxisType::CHAR: - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - for(int i=0; i < chart->_x_axis_s.Size(); i++) - xAxis.add(chart->_x_axis_s[i].c_str()); - #else - if (chart->_x_axis_char_ptr != nullptr) { - for(unsigned int i=0; i < chart->_x_axis_ptr_size; i++) - xAxis.add(chart->_x_axis_char_ptr[i]); - } - #endif - break; - case GraphAxisType::STRING: - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - for(int i=0; i < chart->_x_axis_s.Size(); i++) - xAxis.add(chart->_x_axis_s[i].c_str()); - #else - if (chart->_x_axis_s_ptr != nullptr) { - for(unsigned int i=0; i < chart->_x_axis_ptr_size; i++) - xAxis.add(chart->_x_axis_s_ptr[i]); - } - #endif - break; - default: - // blank value - break; - } - } - - if(!change_only || chart->_y_changed) { - JsonArray yAxis = doc["y"].to(); - switch (chart->_y_axis_type) { - case GraphAxisType::INTEGER: - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - for(int i=0; i < chart->_y_axis_i.Size(); i++) - yAxis.add(chart->_y_axis_i[i]); - #else - if (chart->_y_axis_i_ptr != nullptr) { - for(unsigned int i=0; i < chart->_y_axis_ptr_size; i++) - yAxis.add(chart->_y_axis_i_ptr[i]); - } - #endif - break; - case GraphAxisType::FLOAT: - #if DASH_USE_LEGACY_CHART_STORAGE == 1 - for(int i=0; i < chart->_y_axis_f.Size(); i++) - yAxis.add(chart->_y_axis_f[i]); - #else - if (chart->_y_axis_f_ptr != nullptr) { - for(unsigned int i=0; i < chart->_y_axis_ptr_size; i++) - yAxis.add(chart->_y_axis_f_ptr[i]); - } - #endif - break; - default: - // blank value - break; - } + assert(false); + return ""; } } @@ -514,41 +293,27 @@ void ESPDash::sendUpdates(bool force) { } if (_beforeUpdateCallback) _beforeUpdateCallback(!force); - generateLayoutJSON(nullptr, !force); -} - -void ESPDash::refreshCard(Card *card) { - _ws->cleanupClients(DASH_MAX_WS_CLIENTS); - if (!hasClient()) { - return; - } - if (_beforeUpdateCallback) - _beforeUpdateCallback(true); - generateLayoutJSON(nullptr, true, card); + generateLayoutJSON(nullptr, !force, nullptr); } -void ESPDash::refreshChart(Chart* chart) { +void ESPDash::refresh(const dash::Component& component) { _ws->cleanupClients(DASH_MAX_WS_CLIENTS); if (!hasClient()) { return; } if (_beforeUpdateCallback) _beforeUpdateCallback(true); - generateLayoutJSON(nullptr, true, nullptr, chart); -} - -uint32_t ESPDash::nextId() { - return _idCounter++; -} - -bool ESPDash::hasClient() { - return _ws->count() > 0; + generateLayoutJSON(nullptr, true, &component); } /* Destructor */ -ESPDash::~ESPDash(){ +ESPDash::~ESPDash() { _server->removeHandler(_ws); delete _ws; + _components.clear(); + for (auto c : _componentsOwned) + delete c; + _componentsOwned.clear(); } diff --git a/src/ESPDash.h b/src/ESPDash.h index 6f255c71..a2dea9df 100644 --- a/src/ESPDash.h +++ b/src/ESPDash.h @@ -14,77 +14,39 @@ Github URL: https://github.com/ayushsharma82/ESP-DASH #ifndef ESPDash_h #define ESPDash_h -#include -#include -#include -#include - #include "Arduino.h" -#include "stdlib_noniso.h" #include "dash_webpage.h" -#include "vector.h" +#include "stdlib_noniso.h" #if defined(ESP8266) - #define DASH_HARDWARE "ESP8266" - #include "ESP8266WiFi.h" - #include "ESPAsyncTCP.h" + #define DASH_HARDWARE "ESP8266" + #include "ESP8266WiFi.h" + #include "ESPAsyncTCP.h" #elif defined(ESP32) - #define DASH_HARDWARE "ESP32" - #include "WiFi.h" - #include "AsyncTCP.h" + #define DASH_HARDWARE "ESP32" + #include "AsyncTCP.h" + #include "WiFi.h" #endif -#define DASH_STATUS_IDLE "i" -#define DASH_STATUS_SUCCESS "s" -#define DASH_STATUS_WARNING "w" -#define DASH_STATUS_DANGER "d" - -#include "ESPAsyncWebServer.h" #include "ArduinoJson.h" +#include "ESPAsyncWebServer.h" + +#include "dash/DashDefines.h" +#include "dash/DashCards.h" +#include "dash/DashCharts.h" +#include "dash/DashComponent.h" +#include "dash/DashStatistics.h" +#include "dash/DashWidget.h" + +// deprecated classes, still there for backward compatibility #include "Card.h" #include "Chart.h" #include "Statistic.h" -// If DASH_JSON_SIZE is set to a value, ESP-DASH will frequently measure the Json payload to make sure it remains within this size. -// If the Json payload to send is larger, the payload will be split in several parts and sent in multiple messages. -// -// When this value is set: -// - it should not be too large to avoid sending a big message, which takes longer to send and to build because of the frequent json size measurements. 4096 and 8192 are good values for large dashboards. -// - it should not be too small to avoid sending too many messages, which can slow down the dashboard rendering and fill the websocket message queue. 2048 is a good minimum value. -// -// When using ArduinoJson 7, you can set this value to 0 (by default) to disable the websocket message fragmentation in smaller parts and to disable the measurements, to improve performance. -// This will speed up the rendering, at the expense of risking to exhaust the heap in the case of large dashboard. -// To workaround that, when using DASH_JSON_SIZE == 0 with ArduinoJson 7, you can also set DASH_MIN_FREE_HEAP to a value which is more than the size of the biggest payload for your dashboard. -// For example, if your app is big and has a payload sie of 12kb, then you can set DASH_MIN_FREE_HEAP to 16384 (16kb) to make sure the heap is never exhausted. -// When DASH_MIN_FREE_HEAP is set to a value, you instruct ESP-DASH to check the free heap to make sure there is enough heap to send the payload. -// -// In summary: -// -// - With ArduinoJson 6: DASH_JSON_SIZE should be set to a value greater than 0 and fragmentation is used. -// - With ArduinoJson 7: DASH_JSON_SIZE can be set to 0 to disable the fragmentation and the measurements, at the risk of going out of heap. This option gives the best performance but you need to make sure to have enough heap in case your application is large. -// - With ArduinoJson 7: if DASH_JSON_SIZE is set to 0 and DASH_MIN_FREE_HEAP is set to a value greater than 0, heap will be checked before sending the payload and fragmentation will trigger if not enough heap.. -// - DASH_MIN_FREE_HEAP will have no effect is DASH_JSON_SIZE is not set to 0 -// -// To help you decide, you can uncomment line 543 in the cpp which will display the free heap size and teh required heap size to build the websocket message. -// ESP_LOGW("ESPDash", "Required Heap size to build WebSocket message: %d bytes. Free Heap: %" PRIu32 " bytes", len, ESP.getFreeHeap()); +// Controls the payload size: as soon as the payload size reaches this value, the payload is sent to the client +// This allows to split in batches the payload to avoid sending too large payloads at once #ifndef DASH_JSON_SIZE -#if ARDUINOJSON_VERSION_MAJOR == 6 && !defined(DASH_JSON_DOCUMENT_ALLOCATION) -#define DASH_JSON_SIZE 2048 -#else -#define DASH_JSON_SIZE 0 -#endif // ARDUINOJSON_VERSION_MAJOR == 6 && !defined(DASH_JSON_DOCUMENT_ALLOCATION) -#endif // DASH_JSON_SIZE - -// Only for ArduinoJson 7 -#ifndef DASH_MIN_FREE_HEAP -#define DASH_MIN_FREE_HEAP 0 -#endif - -#if ARDUINOJSON_VERSION_MAJOR == 6 && !defined(DASH_JSON_DOCUMENT_ALLOCATION) -#if DASH_JSON_SIZE == 0 -#error "DASH_JSON_SIZE must be set to a value greater than 0 when using ArduinoJson 6" -#endif -#define DASH_JSON_DOCUMENT_ALLOCATION DASH_JSON_SIZE * 3 + #define DASH_JSON_SIZE 2048 #endif #ifndef DASH_USE_LEGACY_CHART_STORAGE @@ -95,13 +57,8 @@ Github URL: https://github.com/ayushsharma82/ESP-DASH #define DASH_MAX_WS_CLIENTS DEFAULT_MAX_WS_CLIENTS #endif -// Forward Declaration -class Card; -class Chart; -class Statistic; - // ESPDASH Class -class ESPDash{ +class ESPDash { public: // changes_only: true (equivalent to sendUpdates(false)) - when sending updates to the client // changes_only: false (equivalent to sendUpdates(true)) - when sending the entire layout to the client or when forcing a full update @@ -111,62 +68,45 @@ class ESPDash{ AsyncWebServer* _server = nullptr; AsyncWebSocket* _ws = nullptr; - Vector cards; - Vector charts; - Vector statistics; - bool default_stats_enabled = false; + std::list _components; // all components + std::list _componentsOwned; // components created by ESPDash (like statistics), which should be deleted in the destructor bool basic_auth = false; - String username; - String password; + dash::string username; + dash::string password; uint32_t _idCounter = 0; BeforeUpdateCallback _beforeUpdateCallback = nullptr; volatile bool _asyncAccessInProgress = false; // Generate layout json - void generateLayoutJSON(AsyncWebSocketClient* client, bool changes_only = false, Card* onlyCard = nullptr, Chart* onlyChart = nullptr); + void generateLayoutJSON(AsyncWebSocketClient* client, bool changes_only, const dash::Component* onlyComponent); + size_t generateLayoutJSON(AsyncWebSocketClient* client, bool changes_only, const dash::Component* onlyComponent, JsonDocument& doc, dash::Component::Family family); void send(AsyncWebSocketClient* client, JsonDocument& doc); - bool overflowed(JsonDocument& doc); - // Generate Component JSON - void generateComponentJSON(JsonObject& obj, Card* card, bool change_only = false); - void generateComponentJSON(JsonObject& obj, Chart* chart, bool change_only = false); + static const char* jsonKey(const dash::Component* c) { return jsonKey(c->family()); } + static const char* jsonKey(dash::Component::Family family); public: - ESPDash(AsyncWebServer* server, const char* uri, bool enable_default_stats = true); - ESPDash(AsyncWebServer* server, bool enable_default_stats); - ESPDash(AsyncWebServer* server); + ESPDash(AsyncWebServer& server, const char* uri, bool enable_default_stats); + ESPDash(AsyncWebServer& server, bool enable_default_stats) : ESPDash(server, "/", enable_default_stats) {} + ESPDash(AsyncWebServer& server) : ESPDash(server, "/", true) {} // Set Authentication void setAuthentication(const char* user, const char* pass); - void setAuthentication(const String &user, const String &pass); + void setAuthentication(const dash::string& user, const dash::string& pass) { setAuthentication(user.c_str(), pass.c_str()); } - // Add Card - void add(Card *card); - // Remove Card - void remove(Card *card); - - // Add Chart - void add(Chart *card); - // Remove Chart - void remove(Chart *card); - - // Add Statistic - void add(Statistic *statistic); - // Remove Statistic - void remove(Statistic *statistic); + // Add a component to the dashboard and return true if the component was added, false if the component ID was already present + bool add(dash::Component& component); + void remove(dash::Component& component); // Notify client side to update values void sendUpdates(bool force = false); void refreshLayout() { sendUpdates(true); } - void refreshCard(Card *card); - void refreshChart(Chart* chart); + void refresh(const dash::Component& component); - uint32_t nextId(); + bool hasClient() { return _ws->count() > 0; } - bool hasClient(); - - // can be used to check if the async_http task might currently access the cards data, + // can be used to check if the async_http task might currently access the cards data, // in which case you should not modify them bool isAsyncAccessInProgress() { return _asyncAccessInProgress; } @@ -177,6 +117,33 @@ class ESPDash{ void onBeforeUpdate(BeforeUpdateCallback callback) { _beforeUpdateCallback = callback; } ~ESPDash(); + + // deprecations +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + [[deprecated("use ESPDash(AsyncWebServer&, const char*, bool) instead")]] + ESPDash(AsyncWebServer* server, const char* uri, bool enable_default_stats) : ESPDash(*server, uri, enable_default_stats) {} + [[deprecated("use ESPDash(AsyncWebServer&, bool) instead")]] + ESPDash(AsyncWebServer* server, bool enable_default_stats) : ESPDash(*server, enable_default_stats) {} + [[deprecated("use ESPDash(AsyncWebServer&) instead")]] + ESPDash(AsyncWebServer* server) : ESPDash(*server) {} + [[deprecated("use add(dash::Component&) instead")]] + void add(Card* card) { add(*card); } + [[deprecated("use remove(dash::Component&) instead")]] + void remove(Card* card) { remove(*card); } + [[deprecated("use add(dash::Component&) instead")]] + void add(Chart* chart) { add(*chart); } + [[deprecated("use remove(dash::Component&) instead")]] + void remove(Chart* chart) { remove(*chart); } + [[deprecated("use add(dash::Component&) instead")]] + void add(Statistic* statistic) { add(*statistic); } + [[deprecated("use remove(dash::Component&) instead")]] + void remove(Statistic* statistic) { remove(*statistic); } + [[deprecated("use refresh(dash::Component&) instead")]] + void refreshCard(Card* card) { refresh(*card); } + [[deprecated("use refresh(dash::Component&) instead")]] + void refreshChart(Chart* chart) { refresh(*chart); } +#pragma GCC diagnostic pop }; #endif diff --git a/src/Statistic.cpp b/src/Statistic.cpp deleted file mode 100644 index 46995903..00000000 --- a/src/Statistic.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "Statistic.h" - -Statistic::Statistic(ESPDash *dashboard, const char *key, const char *value) { - _dashboard = dashboard; - _id = dashboard->nextId(); - // Safe copy - _key = key; - _value = value; - _dashboard->add(this); -} - -void Statistic::set(const char *value) { - // Safe copy - _changed = _value != value; - if(_changed) - _value = value; -} - -Statistic::~Statistic() { - _dashboard->remove(this); -} \ No newline at end of file diff --git a/src/Statistic.h b/src/Statistic.h index 819343a5..b0d5051d 100644 --- a/src/Statistic.h +++ b/src/Statistic.h @@ -1,30 +1,18 @@ #ifndef __STAT_H #define __STAT_H -#include -#include "Arduino.h" -#include "vector.h" +#include "dash/DashStatistics.h" -#include "ESPDash.h" -#include "ArduinoJson.h" +class [[deprecated("This class is deprecated. Use dash::StatisticValue instead.")]] Statistic : public dash::StatisticValue { + private: + ESPDash* _dashboard; -// Forward Declaration -class ESPDash; - -class Statistic { - private: - ESPDash *_dashboard; - uint32_t _id; - const char *_key; - String _value; - bool _changed = false; - - public: - Statistic(ESPDash *dashboard, const char *key, const char *value = ""); - void set(const char *value); - ~Statistic(); - - friend class ESPDash; + public: + Statistic(ESPDash* dashboard, const char* key, const char* value = "") : dash::StatisticValue(*dashboard, key), _dashboard(dashboard) { set(value); } + bool set(const char* value) { return dash::StatisticValue::setValue(dash::string(value)); } + void set(const dash::string& value) { set(value.c_str()); } + void set(dash::string&& value) { dash::StatisticValue::setValue(std::move(value)); } + ~Statistic(); }; #endif \ No newline at end of file diff --git a/src/dash/DashCards.h b/src/dash/DashCards.h new file mode 100644 index 00000000..34c56228 --- /dev/null +++ b/src/dash/DashCards.h @@ -0,0 +1,379 @@ +#pragma once + +#include "DashWidget.h" + +namespace dash { + enum class Status { + NONE, + IDLE, + SUCCESS, + WARNING, + DANGER, + }; + + // this is the base class for all cards + class Card : public Widget { + public: + virtual ~Card() = default; + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + Widget::toJson(json, onlyChanges); + } + + protected: + // construct a new card and add it to the dashboard + Card(ESPDash& dashboard, const char* name, Type type) : Widget(dashboard, name, type) { + setIndex(255); + } + // construct a new card without adding it to any dashboard + Card(const char* name, Type type) : Widget(name, type) { + setIndex(255); + } + + // status names + static const char* _statusName(Status status) { + switch (status) { + case Status::NONE: + return ""; + case Status::IDLE: + return "i"; + case Status::SUCCESS: + return "s"; + case Status::WARNING: + return "w"; + case Status::DANGER: + return "d"; + default: + assert(false); + return ""; + } + } + }; + + // this is a card holding a value + template + class ValueCard : public Card { + public: + virtual ~ValueCard() = default; + + bool hasValue() const { return _value.has_value(); } + const T& value() const { return _value.value(); } + const std::optional& optional() const { return _value; } + + virtual bool setValue(const T& value) { + if (_value == value) + return false; + _value = value; + setChange(Property::VALUE); + return true; + } + + virtual bool setValue(T&& value) { + if (_value == value) + return false; + _value = std::forward(value); + setChange(Property::VALUE); + return true; + } + + virtual bool setOptionalValue(const std::optional& value) { + return value.has_value() ? setValue(value.value()) : removeValue(); + } + + virtual bool setOptionalValue(std::optional&& value) { + return value.has_value() ? setValue(value.value()) : removeValue(); + } + + virtual bool removeValue() { + if (!_value.has_value()) + return false; + _value.reset(); + setChange(Property::VALUE); + return true; + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + Card::toJson(json, onlyChanges); + if (!onlyChanges || hasChanged(Property::VALUE)) { + if (_value.has_value()) + toJsonValue(json["v"].to(), _value.value()); + else + json["v"] = ""; + } + } + + protected: + ValueCard(ESPDash& dashboard, const char* name, Type type) : Card(dashboard, name, type) {} + ValueCard(const char* name, Type type) : Card(name, type) {} + + private: + std::optional _value; + }; + + // generic card + template || std::is_floating_point_v || std::is_same_v || std::is_same_v, bool> = true> + class GenericCard : public ValueCard { + public: + GenericCard(ESPDash& dashboard, const char* name, const char* symbol = "") : ValueCard(dashboard, name, Component::Type::CARD_GENERIC), _symbol(symbol) {} + GenericCard(const char* name, const char* symbol = "") : ValueCard(name, Component::Type::CARD_GENERIC), _symbol(symbol) {} + virtual ~GenericCard() = default; + + const char* symbol() const { return _symbol; } + + bool setSymbol(const char* symbol) { + if (strcmp(_symbol, symbol) == 0) + return false; + _symbol = symbol; + ValueCard::setChange(dash::Component::Property::SYMBOL); + return true; + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ValueCard::toJson(json, onlyChanges); + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::SYMBOL)) + json["s"] = _symbol; + } + + private: + const char* _symbol; + }; + + // temperature card + template || std::is_floating_point_v, bool> = true> + class TemperatureCard : public ValueCard { + public: + TemperatureCard(ESPDash& dashboard, const char* name, const char* unit = "°C") : ValueCard(dashboard, name, dash::Component::Type::CARD_TEMPERATURE), _unit(unit) {} + TemperatureCard(const char* name, const char* unit = "°C") : ValueCard(name, dash::Component::Type::CARD_TEMPERATURE), _unit(unit) {} + + const char* unit() const { return _unit; } + + bool setUnit(const char* unit) { + if (strcmp(_unit, unit) == 0) + return false; + _unit = unit; + ValueCard::setChange(dash::Component::Property::SYMBOL); + return true; + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ValueCard::toJson(json, onlyChanges); + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::SYMBOL)) + json["s"] = _unit; + } + + private: + const char* _unit; + }; + + // humidity card + template || std::is_floating_point_v, bool> = true> + class HumidityCard : public ValueCard { + public: + HumidityCard(ESPDash& dashboard, const char* name, const char* unit = "%") : ValueCard(dashboard, name, dash::Component::Type::CARD_HUMIDITY), _unit(unit) {} + HumidityCard(const char* name, const char* unit = "%") : ValueCard(name, dash::Component::Type::CARD_HUMIDITY), _unit(unit) {} + + const char* unit() const { return _unit; } + + bool setUnit(const char* unit) { + if (strcmp(_unit, unit) == 0) + return false; + _unit = unit; + ValueCard::setChange(dash::Component::Property::SYMBOL); + return true; + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ValueCard::toJson(json, onlyChanges); + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::SYMBOL)) + json["s"] = _unit; + } + + private: + const char* _unit; + }; + + // feedback card + template || std::is_same_v, bool> = true> + class FeedbackCard : public ValueCard { + public: + FeedbackCard(ESPDash& dashboard, const char* name, Status initialStatus = Status::NONE, const char* initialMessage = "") : ValueCard(dashboard, name, dash::Component::Type::CARD_STATUS), _status(initialStatus) { + setMessage(initialMessage); + } + FeedbackCard(const char* name, Status initialStatus = Status::NONE, const char* initialMessage = "") : ValueCard(name, dash::Component::Type::CARD_STATUS), _status(initialStatus) { + setMessage(initialMessage); + } + virtual ~FeedbackCard() = default; + + Status status() const { return _status; } + + bool setStatus(Status status) { + if (_status == status) + return false; + _status = status; + ValueCard::setChange(Component::Property::SYMBOL); + return true; + } + + bool setMessage(const char* message) { return ValueCard::setValue(message); } + + bool setFeedback(const T& message, Status status) { return setStatus(status) | ValueCard::setValue(message); } + bool setFeedback(T&& message, Status status) { return setStatus(status) | ValueCard::setValue(std::move(message)); } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ValueCard::toJson(json, onlyChanges); + if (!onlyChanges || ValueCard::hasChanged(Component::Property::SYMBOL)) + json["s"] = Card::_statusName(_status); + } + + private: + Status _status; + }; + + // switch card + class SwitchCard : public ValueCard { + public: + SwitchCard(ESPDash& dashboard, const char* name) : ValueCard(dashboard, name, dash::Component::Type::CARD_BUTTON) {} + SwitchCard(const char* name) : ValueCard(name, dash::Component::Type::CARD_BUTTON) {} + virtual ~SwitchCard() = default; + + bool toggle() { return ValueCard::setValue(!ValueCard::optional().value_or(false)); } + bool on() { return ValueCard::setValue(true); } + bool off() { return ValueCard::setValue(false); } + + void onChange(std::function callback) { _callback = callback; } + + virtual void onEvent(const JsonObject& json) override { + if (_callback) + _callback(json["value"].as()); + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ValueCard::toJson(json, onlyChanges); + if (!onlyChanges || hasChanged(Property::VALUE)) + json["v"] = optional().value_or(false) ? 1 : 0; + } + + private: + std::function _callback = nullptr; + }; + + // progress card + template || std::is_floating_point_v, bool> = true> + class ProgressCard : public ValueCard { + public: + ProgressCard(ESPDash& dashboard, const char* name, T minValue, T maxValue, const char* unit = "") : ProgressCard(dashboard, name, dash::Component::Type::CARD_PROGRESS, minValue, maxValue, unit) {} + ProgressCard(const char* name, T minValue, T maxValue, const char* unit = "") : ProgressCard(name, dash::Component::Type::CARD_PROGRESS, minValue, maxValue, unit) {} + virtual ~ProgressCard() = default; + + T min() const { return _minValue; } + T max() const { return _maxValue; } + const char* unit() const { return _unit; } + + bool setUnit(const char* unit) { + if (strcmp(_unit, unit) == 0) + return false; + _unit = unit; + ValueCard::setChange(dash::Component::Property::SYMBOL); + return true; + } + + bool setMin(T minValue) { + if (_minValue == minValue) + return false; + _minValue = minValue; + ValueCard::setChange(dash::Component::Property::MIN); + if (ValueCard::hasValue() && ValueCard::value() < _minValue) + ValueCard::setValue(_minValue); + return true; + } + + bool setMax(T maxValue) { + if (_maxValue == maxValue) + return false; + _maxValue = maxValue; + ValueCard::setChange(dash::Component::Property::MAX); + if (ValueCard::hasValue() && ValueCard::value() > _maxValue) + ValueCard::setValue(_maxValue); + return true; + } + + virtual bool setValue(const T& value) override { + if (value < _minValue) + return ValueCard::setValue(_minValue); + if (value > _maxValue) + return ValueCard::setValue(_maxValue); + return ValueCard::setValue(value); + } + + virtual bool setValue(T&& value) override { + if (value < _minValue) + return ValueCard::setValue(_minValue); + if (value > _maxValue) + return ValueCard::setValue(_maxValue); + return ValueCard::setValue(std::forward(value)); + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ValueCard::toJson(json, onlyChanges); + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::SYMBOL)) + json["s"] = _unit; + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::MIN)) + dash::toJsonValue(json["min"].to(), _minValue); + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::MAX)) + dash::toJsonValue(json["max"].to(), _maxValue); + } + + protected: + ProgressCard(ESPDash& dashboard, const char* name, dash::Component::Type type, T minValue, T maxValue, const char* unit = "") : ValueCard(dashboard, name, type), _minValue(minValue), _maxValue(maxValue), _unit(unit) {} + ProgressCard(const char* name, dash::Component::Type type, T minValue, T maxValue, const char* unit = "") : ValueCard(name, type), _minValue(minValue), _maxValue(maxValue), _unit(unit) {} + + private: + T _minValue; + T _maxValue; + const char* _unit; + }; + + // slider card + template || std::is_floating_point_v, bool> = true> + class SliderCard : public ProgressCard { + public: + SliderCard(ESPDash& dashboard, const char* name, T minValue, T maxValue, T step, const char* unit = "") : ProgressCard(dashboard, name, dash::Component::Type::CARD_SLIDER, minValue, maxValue, unit), _step(step) {} + SliderCard(const char* name, T minValue, T maxValue, T step, const char* unit = "") : ProgressCard(name, dash::Component::Type::CARD_SLIDER, minValue, maxValue, unit), _step(step) {} + virtual ~SliderCard() = default; + + T step() const { return _step; } + + bool setStep(T step) { + if (_step == step) + return false; + _step = step; + ProgressCard::setChange(dash::Component::Property::STEP); + return true; + } + + void onChange(std::function callback) { _callback = callback; } + + virtual void onEvent(const JsonObject& json) override { + if (_callback) + _callback(json["value"].as()); + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + ProgressCard::toJson(json, onlyChanges); + if (!onlyChanges || ValueCard::hasChanged(dash::Component::Property::STEP)) + dash::toJsonValue(json["step"].to(), _step); + } + + private: + T _step; + std::function _callback = nullptr; + }; + + template || std::is_floating_point_v, bool> = true> + class PercentageSliderCard : public SliderCard { + public: + PercentageSliderCard(ESPDash& dashboard, const char* name) : SliderCard(dashboard, name, 0, 100, 1, "%") {} + PercentageSliderCard(const char* name) : SliderCard(name, 0, 100, 1, "%") {} + virtual ~PercentageSliderCard() = default; + }; +} // namespace dash diff --git a/src/dash/DashCharts.h b/src/dash/DashCharts.h new file mode 100644 index 00000000..0b50ae55 --- /dev/null +++ b/src/dash/DashCharts.h @@ -0,0 +1,67 @@ +#pragma once + +#include "DashWidget.h" + +namespace dash { + template || std::is_floating_point_v || std::is_same_v || std::is_same_v) && (std::is_integral_v || std::is_floating_point_v), bool> = true> + class Chart : public Widget { + public: + virtual ~Chart() = default; + + const X* x() const { return _x_axis_i_ptr; } + const Y* y() const { return _y_axis_i_ptr; } + size_t xSize() const { return _x_axis_ptr_size; } + size_t ySize() const { return _y_axis_ptr_size; } + + // set the x-axis values + bool setX(X arr_x[], size_t x_size) { + _x_axis_i_ptr = arr_x; + _x_axis_ptr_size = x_size; + setChange(Property::AXIS_X); + return true; + } + + // set the y-axis values + bool setY(Y arr_y[], size_t y_size) { + _y_axis_i_ptr = arr_y; + _y_axis_ptr_size = y_size; + setChange(Property::AXIS_Y); + return true; + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + Widget::toJson(json, onlyChanges); + if (!onlyChanges || hasChanged(Property::AXIS_X)) { + JsonArray xAxis = json["x"].to(); + for (size_t i = 0; i < _x_axis_ptr_size; i++) + xAxis.add(_x_axis_i_ptr[i]); + } + if (!onlyChanges || hasChanged(Property::AXIS_Y)) { + JsonArray yAxis = json["y"].to(); + for (size_t i = 0; i < _y_axis_ptr_size; i++) + yAxis.add(_y_axis_i_ptr[i]); + } + } + + protected: + // construct a new chart and add it to the dashboard + Chart(ESPDash& dashboard, const char* name, Type type) : Widget(dashboard, name, type) {} + // construct a new chart without adding it to any dashboard + Chart(const char* name, Type type) : Widget(name, type) {} + + private: + X* _x_axis_i_ptr = nullptr; + Y* _y_axis_i_ptr = nullptr; + size_t _x_axis_ptr_size = 0; + size_t _y_axis_ptr_size = 0; + }; + + // bar chart + template || std::is_floating_point_v || std::is_same_v || std::is_same_v) && (std::is_integral_v || std::is_floating_point_v), bool> = true> + class BarChart : public Chart { + public: + BarChart(ESPDash& dashboard, const char* name) : Chart(dashboard, name, Component::Type::CHART_BAR) {} + BarChart(const char* name) : Chart(name, Component::Type::CHART_BAR) {} + virtual ~BarChart() = default; + }; +} // namespace dash diff --git a/src/dash/DashComponent.h b/src/dash/DashComponent.h new file mode 100644 index 00000000..ea8c4c2d --- /dev/null +++ b/src/dash/DashComponent.h @@ -0,0 +1,148 @@ +#pragma once + +#include "DashDefines.h" + +namespace dash { + class Component { + public: + // general component type + enum class Family { + CARD, + CHART, + STATISTIC, + }; + + // component sub-type + enum class Type { + // cards + CARD_BUTTON, + CARD_GENERIC, + CARD_HUMIDITY, + CARD_PROGRESS, + CARD_SLIDER, + CARD_STATUS, + CARD_TEMPERATURE, + // charts + CHART_BAR, + // statistics + STATISTIC_PROVIDER, + STATISTIC_VALUE, + }; + + // describes the component fields that can be changed + enum Property : uint8_t { + AXIS_X = 1, + AXIS_Y, + INDEX, + MAX, + MIN, + NAME, // name or title + STEP, + SYMBOL, + VALUE, + }; + + virtual ~Component() = default; + + // component ID + uint16_t id() const { return _id; } + // component name or title + const char* name() const { return _name; } + // component type + Type type() const { return _type; } + // check if the component belongs to a specific family (card, chart, statistic, tab) + bool is(Family family) const { + switch (family) { + case Family::CARD: + return _type >= Type::CARD_BUTTON && _type <= Type::CARD_TEMPERATURE; + case Family::CHART: + return _type == Type::CHART_BAR; + case Family::STATISTIC: + return _type >= Type::STATISTIC_PROVIDER && _type <= Type::STATISTIC_VALUE; + default: + return false; + } + } + // get the component family + Family family() const { + if (is(Family::CARD)) + return Family::CARD; + if (is(Family::CHART)) + return Family::CHART; + if (is(Family::STATISTIC)) + return Family::STATISTIC; + // should never happen => crash + assert(false); + return Family::CARD; + } + + // check if one of the component property has changed + bool hasChanged() const { return _bitset >> 1; } + // check if a specific component property has changed + bool hasChanged(Property property) const { return _bitset & (0b1 << property); } + // clear all component change flags. + // hasChanged() will return false after this call + void clearChanges() { _bitset &= 0b1; } + + // change component name (not all components support a name change) + bool setName(const char* name) { + if (strcmp(_name, name) == 0) + return false; + _name = name; + setChange(Property::NAME); + return true; + } + + // auto-update the component internally + // some components like StatisticProvider are capable of automatically updating their values before a dashboard refresh + virtual bool selfUpdate() { return false; } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const { + switch (family()) { + case Family::CARD: + case Family::CHART: + json["id"] = id(); + if (!onlyChanges || hasChanged(Property::NAME)) + json["n"] = name(); + break; + case Family::STATISTIC: + json["i"] = id(); + if (!onlyChanges || hasChanged(Property::NAME)) + json["k"] = name(); + break; + default: + assert(false); + break; + } + } + + virtual void onEvent(__unused const JsonObject& json) {} + + protected: + // construct a new component without adding it to any dashboard + Component(const char* name, Type type) : _id(nextId()), _name(name), _type(type) {} + // construct a new component and add it to the dashboard + Component(ESPDash& dashboard, const char* name, Type type); + + // mark a component property as changed + void setChange(Property property) { + +#ifdef DASH_DEBUG + DASH_LOGD("DashComponent", "[%d] %s : property changed: %d", id(), name(), property); +#endif + _bitset |= (0b1 << property); + } + + // generate a new unique component ID + static uint16_t nextId() { + static uint16_t _IDS = 0; + return _IDS++; + } + + private: + const uint16_t _id; // component ID + const char* _name; // component name + const Type _type; // component type + uint16_t _bitset = 0b1; // display is bit 0, bits 1-12 to track component changes + }; +} // namespace dash diff --git a/src/dash/DashDefines.h b/src/dash/DashDefines.h new file mode 100644 index 00000000..a1760c88 --- /dev/null +++ b/src/dash/DashDefines.h @@ -0,0 +1,95 @@ +#pragma once + +// set to 1 to see debug logs +// keep to 0 for production: logging impacts performance +// #define DASH_DEBUG 1 + +#ifdef ESP8266 + #include + #define DASH_LOGD(tag, format, ...) ets_printf("DEBUG [%s] " format "\n", tag, ##__VA_ARGS__) + #define DASH_LOGW(tag, format, ...) ets_printf("WARN [%s] " format "\n", tag, ##__VA_ARGS__) +#else + #include + #define DASH_LOGD ESP_LOGD + #define DASH_LOGW ESP_LOGW +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include + +class ESPDash; + +namespace dash { +#ifdef DASH_USE_STL_STRING + using string = std::string; +#else + using string = String; +#endif + + // function to convert an unknown type T to a JSON value by applying the correct precision for floats and doubles + template + static bool toJsonValue(const JsonVariant& json, const T& value) { + if constexpr (std::is_same_v) { + return json.set(String(static_cast(value), (size_t)Precision)); + } else if constexpr (std::is_same_v) { + return json.set(String(static_cast(value), (size_t)Precision)); + } else { + // convert unknown other types eventually using convertToJson functions + return json.set(value); + } + } + + // dash::to_string + + template , bool> = true> + static const dash::string& to_string(const T& value) { return value; } + + template , bool> = true> + static dash::string to_string(const T& value) { return value; } + + template , bool> = true> + static dash::string to_string(const T& value) { +#ifdef DASH_USE_STL_STRING + return std::to_string(value); +#else + return String(value); +#endif + } + + template , bool> = true> + static dash::string to_string(const T& value) { +#ifdef DASH_USE_STL_STRING + return String(value, (size_t)Precision).c_str(); +#else + return String(value, (size_t)Precision); +#endif + } + + // dash::string_as + + template || std::is_same_v, bool> = true> + static std::optional string_as(const char* str) { + if (str == nullptr) + return std::nullopt; + return str; + } + + template || std::is_floating_point_v, bool> = true> + static std::optional string_as(const char* str) { + auto value = T{}; + size_t len = strlen(str); + if (!len) + return std::nullopt; + auto [ptr, error] = std::from_chars(str, str + strlen(str), value); + if (error != std::errc()) + return std::nullopt; + return value; + } +} diff --git a/src/dash/DashStatistics.h b/src/dash/DashStatistics.h new file mode 100644 index 00000000..4e59e7d2 --- /dev/null +++ b/src/dash/DashStatistics.h @@ -0,0 +1,89 @@ +#pragma once + +#include "DashComponent.h" + +namespace dash { + // Statistic super class + template || std::is_floating_point_v || std::is_same_v || std::is_same_v, bool> = true> + class Statistic : public Component { + public: + virtual ~Statistic() = default; + + // statistic value + const T& value() const { return _value; } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + Component::toJson(json, onlyChanges); + if (!onlyChanges || hasChanged(Property::VALUE)) + dash::toJsonValue(json["v"].to(), _value); + } + + protected: + // construct a new statistic and add it to the dashboard + Statistic(ESPDash& dashboard, const char* name, Type type) : Component(dashboard, name, type) {} + // construct a new statistic without adding it to any dashboard + Statistic(const char* name, Type type) : Component(name, type) {} + + T _value; + }; + + // Statistic value class with set method + template || std::is_floating_point_v || std::is_same_v || std::is_same_v, bool> = true> + class StatisticValue : public Statistic { + public: + // construct a new statistic and add it to the dashboard + StatisticValue(ESPDash& dashboard, const char* name) : Statistic(dashboard, name, Component::Type::STATISTIC_VALUE) {} + // construct a new statistic without adding it to any dashboard + StatisticValue(const char* name) : Statistic(name, Component::Type::STATISTIC_VALUE) {} + + ~StatisticValue() = default; + + // update the statistic value and returns true if the value has changed + bool setValue(const T& value) { + if (Statistic::_value == value) + return false; + Statistic::_value = value; + Component::setChange(Component::Property::VALUE); + return true; + } + + // update the statistic value and returns true if the value has changed + bool setValue(T&& value) { + if (Statistic::_value == value) + return false; + Statistic::_value = std::forward(value); + Component::setChange(Component::Property::VALUE); + return true; + } + }; + + // Statistic provider class where the value if provided by a function + template || std::is_floating_point_v || std::is_same_v || std::is_same_v, bool> = true> + class StatisticProvider : public Statistic { + private: + std::function _provider = nullptr; + + public: + // construct a new statistic and add it to the dashboard + StatisticProvider(ESPDash& dashboard, const char* name) : Statistic(dashboard, name, Component::Type::STATISTIC_PROVIDER) {} + // construct a new statistic without adding it to any dashboard + StatisticProvider(const char* name) : Statistic(name, Component::Type::STATISTIC_PROVIDER) {} + + ~StatisticProvider() = default; + + // register a provider from where to get the statistic value + void setProvider(std::function provider) { _provider = provider; } + + // update the statistic value by calling the provider and returns true if the value has changed + virtual bool selfUpdate() override { + if (!_provider) + return false; + T value = _provider(); + if (Statistic::_value == value) + return false; + Statistic::_value = std::forward(value); + Component::setChange(Component::Property::VALUE); + return true; + } + }; +} // namespace dash diff --git a/src/dash/DashWidget.h b/src/dash/DashWidget.h new file mode 100644 index 00000000..8483f675 --- /dev/null +++ b/src/dash/DashWidget.h @@ -0,0 +1,61 @@ +#pragma once + +#include "DashComponent.h" + +namespace dash { + class Widget : public Component { + public: + Widget(const char* name, Type type) : Component(name, type) {} + Widget(ESPDash& dashboard, const char* name, Type type) : Component(dashboard, name, type) {} + + virtual ~Widget() {} + + // widget index (position) in the dashboard + uint8_t index() const { return _index; } + + // set the widget position in the dashboard + bool setIndex(uint8_t index) { + if (_index == index) + return false; + _index = index; + setChange(Property::INDEX); + return true; + } + + virtual void toJson(const JsonObject& json, bool onlyChanges) const override { + Component::toJson(json, onlyChanges); + if (!onlyChanges || hasChanged(Property::INDEX)) + json["t"] = _typeName(type()); + if (!onlyChanges || hasChanged(Property::INDEX)) + json["idx"] = _index; + } + + private: + uint8_t _index; + + // widget type name + static const char* _typeName(Type type) { + switch (type) { + case Type::CARD_BUTTON: + return "button"; + case Type::CARD_GENERIC: + return "generic"; + case Type::CARD_HUMIDITY: + return "humidity"; + case Type::CARD_PROGRESS: + return "progress"; + case Type::CARD_SLIDER: + return "slider"; + case Type::CARD_STATUS: + return "status"; + case Type::CARD_TEMPERATURE: + return "temperature"; + case Type::CHART_BAR: + return "bar"; + default: + assert(false); + return ""; + } + } + }; +} // namespace dash diff --git a/src/vector.h b/src/vector.h deleted file mode 100644 index 1c55e7f2..00000000 --- a/src/vector.h +++ /dev/null @@ -1,243 +0,0 @@ -#ifndef VectorClass -#define VectorClass - - -#ifndef MIN -#define MIN(a, b) (((a) < (b)) ? (a) : (b)) -#endif - -#ifndef MAX -#define MAX(a, b) (((a) > (b)) ? (a) : (b)) -#endif - -#define SWAP(type, a, b) type tmp ## a = a; a = b; b = tmp ## a; - -// This class implements missing vector from STL -template class Vector -{ - VectorType *begin; - VectorType *storage; - int head; - -public: - VectorType OB; - - // We can save a few re-sizings if we know how large the array is likely to grow to be - Vector(int initialSize = 0) - { - begin = new VectorType[initialSize]; //points to the beginning of the new array - head = initialSize - 1; - storage = begin + initialSize; //points to the element one outside of the array (such that end - begin = capacity) - } - - Vector(Vector &obj) - { - begin = new VectorType[0]; // Points to the beginning of the new array, it's zero but this line keeps malloc from seg faulting should we delete begin before resizing it - head = -1; - storage = begin; //points to the element one outside of the array (such that end - begin = capacity) - - *this = obj; - } - - // If there's anything in the vector then delete the array, if there's no array then doing will will cause seg faults - virtual ~Vector() { delete[] begin; } - - Vector &operator=(Vector &obj) - { - // Reallocate the underlying buffer to the same size as the - Resize(obj.Size()); - - for(int i = 0; i < obj.Size(); i++) - (*this)[i] = obj[i]; - - head = obj.head; - - return *this; - } - - // Swaps the underlying array and characteristics of this vector with another of the same type, very quickly - void Swap(Vector &obj) - { - SWAP(int, head, obj.head); - SWAP(VectorType*, begin, obj.begin); - SWAP(VectorType*, storage, obj.storage); - } - - // Checks the entire Vector to see whether a matching item exists. Bear in mind that the VectorType might need to implement - // equality operator (operator==) for this to work properly. - bool Contains(VectorType element) - { - for(int i = 0; i < Size(); i++) - if(operator [](i) == element) - return true; - - return false; - } - - int Find(VectorType element) - { - for(int i = 0; i < Size(); i++) - if(operator [](i) == element) - return i; - - return -1; - } - - void PushBack(VectorType element) { PushBack(&element, 1); } - - void PushBack(const VectorType *elements, int len) - { - // If the length plus this's size is greater than the capacity, reallocate to that size. - if(len + Size() > Capacity()) - ReAllocate(MAX(Size() + len, Size() * 2)); - - int append = MIN(storage - begin - head - 1, len), prepend = len - append; - - // memcpy the data starting at the head all the way up to the last element *(storage - 1) - memcpy((begin + head + 1), elements, sizeof(VectorType) * append); - - // If there's still data to copy memcpy whatever remains, starting at the first element *(begin) until the end of data. The first step will have ensured - // that we don't crash into the tail during this process. - memcpy(begin,(elements + append), sizeof(VectorType) * prepend); - - // Re-recalculate head and size. - head += len; - } - - void Erase(unsigned int position) { Erase(position, position + 1); } - - // Erase an arbitrary section of the vector from first up to last minus one. Like the stl counterpart, this is pretty labour intensive so go easy on it. - void Erase(int first, int last) - { - // For this we'll set the value of the array at first to the value of the array at last plus one. We'll do that all the way up to toIndex - for(int i = 0; i < (Size() - first); i++) - { - // If by trying to fill in the next element with the ones ahead of it we'll be running off the end of the vector, stop. - if((i + last) > (Size() - 1)) - break; - - begin[first + i] = begin[last + i]; - } - - // Adjust the head to reflect the new size - head -= last - first; - } - - // Remove the most recent element in the array - void PopBack() - { - if(Size() > 0) - head--; - } - - // Empty the vector, or to be precise - forget the fact that there was ever anything in there. - void Clear() { head = -1; } - - // Returns a bool indicating whether or not there are any elements in the array - bool Empty() { return head == -1; } - - // Returns the oldest element in the array (the one added before any other) - VectorType const &Back() { return *begin; } - - // Returns the newest element in the array (the one added after every other) - VectorType const &Front() { return begin[head]; } - - // Returns the nth element in the vector - VectorType &operator[](int n) - { - if(n < Size()) - return begin[n]; - else - return OB; // out of bounds - } - - // Returns a pointer such that the vector's data is laid out between ret to ret + size - VectorType *Data() { return begin; } - - // Recreates the vector to hold len elements, all being copies of val - void Assign(int len, const VectorType &val) - { - delete[] begin; - - // Allocate an array the same size as the one passed in - begin = new VectorType[len]; - storage = begin + len; - - // Refresh the head and tail, assuming the array is in order, which it really has to be - head = len - 1; - - for(int i = 0 ; i < Size(); i++) - begin[i] = val; - } - - // Recreates the vector using an external array - void Assign(VectorType *array, int len) - { - delete[] begin; - - // Allocate an array the same size as the one passed in - begin = new VectorType[len]; - storage = begin + len; - - // Refresh the head and tail, assuming the array is in order, which it really has to be - head = len - 1; - - // Copy over the memory - memcpy(begin, array, sizeof(VectorType) * len); - } - - // Returns the number of elements that the vector will support before needing resizing - int Capacity() { return (storage - begin); } - - // Returns the number of elements in vector - int Size() { return head + 1; } - - // Requests that the capacity of the allocated storage space for the elements - // of the vector be at least enough to hold size elements. - void Reserve(unsigned int size) - { - if(size > Capacity()) - ReAllocate(size); - } - - // Resizes the vector - void Resize(unsigned int size) - { - // If necessary, resize the underlying array to fit the new size - if(size > Capacity()) - ReAllocate(size); - - // Now revise the head and size (tail needn't change) to reflect the new size - head = size - 1; - } - -private: - - void ReAllocate(unsigned int size) - { - // Just in case we're re-allocating less room than we had before, make sure that we don't overrun the buffer by trying to write more elements than - // are now possible for this vector to hold. - if(Size() > (int)size) - head = size - 1; - - // Allocate an array twice the size of that of the old - VectorType *_begin = new VectorType[size]; - VectorType *_storage = _begin + size; - - int _head = Size() - 1; - - // Copy across all the old array's data and rearrange it! - for(int i = 0; i < Size(); i++) - _begin[i] = (*this)[i]; - - // Free the old memory - delete[] begin; - - // Redirect the old array to point to the new one - begin = _begin; - storage = _storage; - head = _head; - } -}; - -#endif \ No newline at end of file diff --git a/v5.md b/v5.md new file mode 100644 index 00000000..1639298a --- /dev/null +++ b/v5.md @@ -0,0 +1,232 @@ +# ESP-DASH v5 + +ESP-DASH v5 is a rewrite of ESP-DASH \*\*OSS and Pro## Motivation versions with C++ 17. + +## Motivation + +The rewrite has been motived by several factors including: + +- the inherent design inefficient of memory usage: widgets are using about 2.5 more RAN memory than they really need +- websocket message optimisation: there are still room to allow more efficient message exchange between the client and the server +- inability to extend the components and behavior +- inability to correctly handle a custom float precision +- inability to support all integral and floating point types +- no namespace isolation +- no STL string support +- pointer vs ref: the old API uses pointers to widgets, which is not idiomatic C++ and can lead to bugs if null is passed. + +The rewrite uses **C++ 17 inheritance, polymorphism and templating**. + +The rewrite is also **backward compatible** and will display **deprecation compiler warnings** if the old deprecated API is used. + +Here is a screenshot of before / after during the development. +Now, the `website.cpp` file is 15KB, compared to the initial 36KB. + +**The preliminary results in a big app with about 225 cards and 30 stats show a decrease in RAM usage of about 60%.** + +![Screenshot 2024-11-29 at 10 07 58](https://github.com/user-attachments/assets/6c91fe35-9527-45d3-8cfb-2287690bbe72) + +## Benchmark5.ino Example + +[Benchmark5.ino](./examples/Benchmark5/Benchmark5.ino) is a new example that demonstrates the new API and the new features. + +## Compile flags + +C++ 17 is required, which is the default in new Arduino versions, otherwise, you can add to your PIO file: + +``` +build_flags = + -std=c++17 + -std=gnu++17 + -Wall -Wextra + ; -D DASH_USE_LEGACY_CHART_STORAGE=1 + ; -D DASH_USE_STL_STRING=1 + ; -D DASH_DEBUG +build_unflags = + -std=gnu++11 +``` + +- `-D DASH_DEBUG`: activate debug mode +- `-D DASH_USE_STL_STRING=1`: uses `std::string` instead of `String` for the string type + +ESP-DASH also defines its own string `dash::string`, which points to `String` or `std::string` depending on the flag `-D DASH_USE_STL_STRING=1`. +`dash::string` can be used to avoid using `String` or `std::string` directly and write portable code or examples. + +## API Changes + +![image](https://github.com/user-attachments/assets/006f3ef1-7d01-4231-8ac7-4cc98df2e63d) + +### All Widgets + +- Widgets generally support templated types: integral, floating point, string (`const char*`, `std::string`, `String`) +- Widgets generally support a custom precision for floating point types (default is 2) + +Note: working with floating point numbers is generally slower than working with integral numbers because of the rounding step requiring to convert the number to a string representation with a fixed number of decimals. + +### Statistics + +- `dash::StatisticValue`: replaces `Statistic` +- `dash::StatisticProvider`: a new kind of auto-updatable statistic: the value is sourced from a function and is automatically updated when the dashboard is refreshed. + +```cpp +// a string based statistic +dash::StatisticValue stat(dashboard, "Client name"); + +// a statistic string based using a constant string pointer +dash::StatisticValue stat(dashboard, "State (on/off)"); + +// a float based statistic with a default precision of 2 decimals +dash::StatisticValue stat(dashboard, "Temperature (°C)"); + +// a float based statistic with a custom precision of 3 decimals +dash::StatisticValue stat(dashboard, "Energy (kWh)"); + +// an integral based statistic +dash::StatisticValue stat(dashboard, "Percent (%)"); + +dash::StatisticProvider statProvider(dashboard, "Uptime (ms)"); +statProvider.setProvider([]() { return millis(); }); +``` + +### Charts + +Charts have 2 axis: X and Y. +For each axis, the type can be integral or floating point. +For the X axis, strings are also supported. + +```cpp +dash::BarChart bar(dashboard, "Power Usage (kWh) per day"); + +dash::BarChart bar(dashboard, "Power Usage (kWh) per hour"); +``` + +**For performance reasons, floating point precision is not supported for charts.** +It is advised to do the rounding in the value arrays. + +### Cards + +- `dash::FeedbackCard` + + - Replaces `STATUS_CARD` + - Defaults to `dash::string` type + - Supports also `const char*` with `dash::FeedbackCard` + - Supports an initial state + +```cpp +dash::FeedbackCard feedback(dashboard, "Status"); +feedback.setFeedback("Light is ON", dash::Status::SUCCESS); +``` + +- `dash::GenericCard` + - Replaces `GENERIC_CARD` + - Defaults to `dash::string` type + - Supports any integral, floating point type or string type + - Supports a custom precision for floating point types + - Supports a symbol or unit + +```cpp +dash::GenericCard generic(dashboard, "Counter", "restarts"); +dash::GenericCard energy(dashboard, "Energy", "kWh"); +dash::GenericCard kp(dashboard, "PID Kp"); +``` + +- `dash::HumidityCard` and `dash::TemperatureCard` + - Replaces `HUMIDITY_CARD` and `TEMPERATURE_CARD` + - Defaults to `float` type with a precision of 2 decimals + - Supports any integral or floating point type + - Supports a custom precision for floating point types. + - Unit is preset to `%` for humidity and `°C` for temperature but can be changed + +```cpp +dash::TemperatureCard temperature(dashboard, "Temperature"); +dash::TemperatureCard temperature(dashboard, "Temperature", "°F"); +``` + +- `dash::ProgressCard` + + - Replaces `PROGRESS_CARD` + - Defaults to `int` type + - Supports any integral or floating point type + - Supports a custom precision for floating point types. + - Supports a symbol or unit + - Allow a configurable min/max range + +```cpp +dash::ProgressCard preciseProgress(dashboard, "Progress", 0.0f, 100.0f, "%"); +``` + +- `dash::SliderCard` + + - Replaces`SLIDER_CARD` + - Defaults to `int` type + - Supports any integral or floating point type + - Supports a custom precision for floating point types. + - Supports a symbol or unit + - Allow a configurable min/max range + - Allow a configurable step + +```cpp +dash::SliderCard duty(dashboard, "Duty", 0, 255, 1, "bits"); +dash::SliderCard kp(dashboard, "PID Kp", 0.0f, 1.0f, 0.001f); +``` + +- `dash::SwitchCard` + - Replaces `BUTTON_CARD` + - Defaults to `bool` type + +```cpp +dash::SwitchCard light(dashboard, "Light"); +``` + +### Functions and callbacks + +- `onChange([]()){}` + +Listen to card changes: + +```cpp +button.onChange([&](bool state) { + /* Print our new button value received from dashboard */ + Serial.println(String("Button Triggered: ") + (state ? "true" : "false")); + /* Make sure we update our button's value and send update to dashboard */ + button.setValue(state); + dashboard.refresh(button); +}); + +updateDelay.onChange([&](uint32_t value) { + update_delay = value; + updateDelay.setValue(value); + dashboard.refresh(updateDelay); +}); +``` + +- `value()`: get the value of a card +- `min()`, `max()`, `step()`: get the min, max, step of a slider card +- `setMin()`, `setMax()`, `setStep()`: set the min, max, step of a slider card +- `setFeedback()`: set the feedback of a feedback card +- `setValue()`: set the value of a card +- etc + +## Optimisations + +By default, the string type that will be used to store string values is `String` or `std::string` if the flag `-D DASH_USE_STL_STRING=1` is set. + +To avoid allocating memory and copying strings, the `const char*` type can be used when the card is sourcing its content from constant strings only. + +**Example:** + +```cpp +dash::FeedbackCard feedback(dashboard, "Status"); // uses c string pointers +dash::FeedbackCard customFeedback(dashboard, "Status"); // uses String or std::string object + +// [...] + +if(lightON) + feedback.setFeedback("Light is ON", dash::Status::SUCCESS); +else + feedback.setFeedback("Light is OFF", dash::Status::ERROR); + +// [...] + +customFeedback.setFeedback(dash::string("Counter: ") + count , dash::Status::WARNING); +```