diff --git a/blueprint/ead_update.yaml b/blueprint/ead_update.yaml index 5a2dcb9..0b91697 100644 --- a/blueprint/ead_update.yaml +++ b/blueprint/ead_update.yaml @@ -25,23 +25,23 @@ blueprint: default: 1 selector: number: - min: -255 - max: 255 + min: -10000 + max: 10000 display_value_max: name: Display Value Max description: The maximum value to display default: 100 selector: number: - min: -255 - max: 255 + min: -10000 + max: 10000 display_segments: name: Amount of segments on face description: How many values to display between the min and max default: 10 selector: number: - min: 2 + min: 1 max: 20 variables: @@ -62,5 +62,5 @@ action: data: topic_template: "{{ 'homeassistant/ead/' ~ topic_name }}" qos: "0" - retain: false + retain: true payload_template: '{{ ''{ "name":"'' ~ display_name ~ ''", "val":"'' ~ states(sensor) ~ ''", "unit":"'' ~ display_unit ~ ''", "min":"'' ~ display_value_min ~ ''", "max":"'' ~ display_value_max ~ ''", "segm":"'' ~ display_segments ~ ''"}'' }}' diff --git a/platformio.ini b/platformio.ini index cc74ec3..91858ec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,7 +8,7 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html -[env:esp] +[env] platform = espressif32 board = esp-wrover-kit framework = arduino @@ -22,3 +22,8 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.7 knolleary/PubSubClient@^2.8 lennarthennigs/Button2@^2.3.2 + bblanchon/ArduinoJson@^7.0.4 + +[env:dev] +build_type = debug +debug_build_flags = -DDEBUG \ No newline at end of file diff --git a/src/einkanalog/einkanalog.cpp b/src/einkanalog/einkanalog.cpp index 76b7018..26ece3a 100644 --- a/src/einkanalog/einkanalog.cpp +++ b/src/einkanalog/einkanalog.cpp @@ -8,7 +8,9 @@ EinkAnalogDisplay::EinkAnalogDisplay(GxEPD2_BW * if (GxEPD_DEBUG) { this->display->init(115200); - } else { + } + else + { this->display->init(0); } @@ -37,7 +39,7 @@ void EinkAnalogDisplay::centerText(const char *text, int16_t x, int16_t y) display->print(text); } -void EinkAnalogDisplay::drawFace(int16_t min, int16_t max, char *name, char* unit, uint8_t lines) +void EinkAnalogDisplay::drawFace(int16_t min, int16_t max, const char *name, const char *unit, uint8_t lines) { this->min = min; this->max = max; @@ -69,7 +71,7 @@ void EinkAnalogDisplay::drawFace(int16_t min, int16_t max, char *name, char* uni const float angle = startAngle + (angleTotal / (float)angleDiv) * i; const float len1 = (armSizeMm / displayMmPx) + 2; // Begin of ind. line - const float len3 = len1 + 6; // Text pos for indicator line + const float len3 = len1 + 6; // Text pos for indicator line const float len2 = (i == 0 || i == angleDiv) ? (len1 - lineSizeEx) : (len1 - lineSizeDef); @@ -130,22 +132,24 @@ void EinkAnalogDisplay::drawFace(int16_t min, int16_t max, char *name, char* uni this->centerText(unit, origin[0], 95); } -void EinkAnalogDisplay::drawMeta(const char *wireless_str) { - // Draw WiFi and Build info +void EinkAnalogDisplay::drawMeta(const char *wireless_str, uint8_t page_cur, uint8_t page_total) +{ + // Draw WiFi and page info display->drawBitmap(0, 114, gfx_wireless, 5, 7, GxEPD_BLACK); display->setFont(&Pixeltype8pt7b); display->setCursor(8, 119); display->print(wireless_str); - char build_str[] = __DATE__; + char page_str[8] = {}; // Should never exceed 8 characters (XX / XX + terminator), more than 99 pages would be impractical + sprintf(page_str, "%d / %d", page_cur, page_total); int16_t tbx, tby; // Text box x, y uint16_t tbw, tbh; // Text box W, H - display->getTextBounds(build_str, 0, 121, &tbx, &tby, &tbw, &tbh); + display->getTextBounds(page_str, 0, 121, &tbx, &tby, &tbw, &tbh); display->drawBitmap(display->width() - 5, 114, gfx_build, 5, 7, GxEPD_BLACK); display->setCursor(display->width() - tbw - 8, 119); - display->print(build_str); + display->print(page_str); } int EinkAnalogDisplay::setArm(float value) @@ -158,8 +162,10 @@ int EinkAnalogDisplay::setArm(float value) // NewValue = (((OldValue - OldMin) * NewRange) / OldRange) + NewMin; // new_value = ((old_value - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min - uint8_t result = round(((float)(value - this->min) / (float)(this->max - this->min)) * 255.0); + uint8_t result = round(((float)((float)(value - this->min) / (float)(this->max - this->min)) * (float)DAC_OFFSET) * 255.0); dacWrite(this->dac, result); + + this->value = value; return 1; } else diff --git a/src/einkanalog/einkanalog.h b/src/einkanalog/einkanalog.h index bf29bca..efba350 100644 --- a/src/einkanalog/einkanalog.h +++ b/src/einkanalog/einkanalog.h @@ -32,9 +32,10 @@ private: public: EinkAnalogDisplay(GxEPD2_BW *display, uint8_t dacPin, bool GxEPD_DEBUG = false); ~EinkAnalogDisplay(); - void drawFace(int16_t min, int16_t max, char *name, char *unit, uint8_t lines = 5); + void drawFace(int16_t min, int16_t max, const char *name, const char *unit, uint8_t lines); int setArm(float value); - void drawMeta(const char *wireless_str); + void drawMeta(const char *wireless_str, uint8_t page_cur, uint8_t page_total); + int16_t value; }; #endif \ No newline at end of file diff --git a/src/global.h b/src/global.h index 55e7454..e4e230e 100644 --- a/src/global.h +++ b/src/global.h @@ -4,9 +4,6 @@ #include #include -// Debug -#define DEBUG 1 - #if DEBUG #define D_SerialBegin(...) Serial.begin(__VA_ARGS__); #define D_print(...) Serial.print(__VA_ARGS__) @@ -17,23 +14,19 @@ #define D_println(...) #endif -// Wifi #include #include -// MQTT #define MQTT_ADDR "192.168.2.196" #define MQTT_PORT 1883 -// DAC #define DAC1 25 +#define DAC_OFFSET 1.05 // Analog pointer is low-key garbage -// Buttons #include "Button2.h" #define BTN_L 22 #define BTN_R 21 -// Local helper files #include "./einkanalog/einkanalog.h" #include "./helpers/mqtt.h" diff --git a/src/helpers/mqtt.cpp b/src/helpers/mqtt.cpp index 0406961..76ec06f 100644 --- a/src/helpers/mqtt.cpp +++ b/src/helpers/mqtt.cpp @@ -6,7 +6,8 @@ MqttTopic::MqttTopic(WiFiClient *espClient) : wifiClient(espClient), client(PubS server.fromString(MQTT_ADDR); this->client.setServer(server, MQTT_PORT); - this->client.setCallback([&](char *a, byte *b, unsigned int c){ this->callback(a, b, c); }); + this->client.setCallback([&](char *a, byte *b, unsigned int c) + { this->callback(a, b, c); }); } MqttTopic::~MqttTopic() @@ -29,22 +30,55 @@ void MqttTopic::callback(char *topic, byte *payload, unsigned int length) D_print("Topic: "); D_println(token); - // Check if subtopic has been found + deserializeJson(this->doc, payload); + + MqttTopicData result = { + .name = {}, + .val = this->doc["val"].as(), + .unit = {}, + .min = this->doc["min"].as(), + .max = this->doc["max"].as(), + .segm = this->doc["segm"].as()}; + + // Strings need to be copied to be able to switch pages + strcpy(result.name, this->doc["name"].as()); + strcpy(result.unit, this->doc["unit"].as()); + + // Check if token exists in topics if (i == 2) { - // Check if token exists in topics - if (std::find(std::begin(topics), std::end(topics), token) != std::end(topics)) + bool tokenExists = false; + size_t j = 0; + for (; j < available; j++) + { + if (strcmp(topics[j], token) == 0) + { + tokenExists = true; + break; + } + } + + if (tokenExists) { // Token exists D_println("Token exists"); + data[j].val = result.val; } else { // Add token to topics - D_print((char*)topics); strcpy(topics[available], token); + data[available] = result; available++; } + + D_println(data[j].name); + D_println(data[j].val); + D_println(data[j].unit); + D_println(data[j].min); + D_println(data[j].max); + D_println(data[j].segm); + D_println(); } else { @@ -52,14 +86,7 @@ void MqttTopic::callback(char *topic, byte *payload, unsigned int length) D_println("Topic invalid"); return; } - - - // char array[length + 1] = {}; - // memcpy(array, payload, length); - // array[length] = '\0'; - // D_println(String(array).toFloat() / 1000.0); - // ead.setArm(String(array).toFloat() / 1000.0); } void MqttTopic::loop() diff --git a/src/helpers/mqtt.h b/src/helpers/mqtt.h index 091d35d..36cb629 100644 --- a/src/helpers/mqtt.h +++ b/src/helpers/mqtt.h @@ -7,22 +7,47 @@ // MQTT #include +// JSON +#include + + +// { +// "name": "Huiskamer", +// "val": "17.98", +// "unit": "°C", +// "min": "0", +// "max": "40", +// "segm": "10" +// } +struct MqttTopicData +{ + char name[16]; // Max 16 characters + null terminator + float val; + char unit[4]; // Max 3 characters + null terminator + int32_t min; + int32_t max; + uint8_t segm; +}; + class MqttTopic { private: WiFiClient* wifiClient; PubSubClient client; + JsonDocument doc; // Home assistant topic should follow /homeassistant/ead/unique_id char topics[10][8]; // Array of topics available, we'll go with 10 for now - uint8_t available = 0; // Topics present - uint8_t current = 0; // Current topic public: MqttTopic(WiFiClient* espClient); ~MqttTopic(); void callback(char *topic, byte *payload, unsigned int length); void loop(); + + MqttTopicData data[10]; // Data for each topic + uint8_t available = 0; // Topics present + uint8_t current = 0; // Current topic }; #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c7849b5..2659469 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,32 +3,14 @@ WiFiMulti WiFiMulti; WiFiClient espClient; - -// Buttons Button2 btn_L, btn_R; - -// Display GxEPD2_BW display(GxEPD2_213_B72(/*CS=5*/ SS, /*DC=*/17, /*RST=*/16, /*BUSY=*/4)); -// Helpers EinkAnalogDisplay ead(&display, DAC1); MqttTopic mqtt(&espClient); -void btnHandler(Button2 &btn) -{ - switch (btn.getType()) - { - case single_click: - break; - case double_click: - D_print("double "); - break; - } - D_print("click "); - D_print("on button #"); - D_print((btn == btn_L) ? "L" : "R"); - D_println(); -} +void updateView(); +void btnHandler(Button2 &btn); void setup() { @@ -73,10 +55,14 @@ void setup() esp_deep_sleep_start(); } - display.fillScreen(GxEPD_WHITE); - ead.drawFace(0, 9, "Solax Prod.", "kW", 10); - ead.drawMeta(WiFi.localIP().toString().c_str()); - display.nextPage(); + // Wait for content + while (mqtt.available == 0) + { + mqtt.loop(); + delay(100); + } + + updateView(); } void loop() @@ -84,4 +70,62 @@ void loop() btn_L.loop(); btn_R.loop(); mqtt.loop(); + + // Only update arm if topic has not been changed + if (mqtt.data[mqtt.current].val != ead.value) + { + ead.setArm(mqtt.data[mqtt.current].val); + } +} + + +void updateView() +{ + display.fillScreen(GxEPD_WHITE); + + // Update display + ead.drawFace( + mqtt.data[mqtt.current].min, + mqtt.data[mqtt.current].max, + mqtt.data[mqtt.current].name, + mqtt.data[mqtt.current].unit, + mqtt.data[mqtt.current].segm + ); + ead.drawMeta(WiFi.localIP().toString().c_str(), mqtt.current + 1, mqtt.available); + + // Update arm + ead.setArm(mqtt.data[mqtt.current].val); + + display.nextPage(); +} + +void btnHandler(Button2 &btn) +{ + switch (btn.getType()) + { + case single_click: + if (btn == btn_L) + { + mqtt.current = (mqtt.current - 1) % mqtt.available; + } + else + { + mqtt.current = (mqtt.current + 1) % mqtt.available; + } + updateView(); + break; + case double_click: + if (btn == btn_L) + { + display.fillScreen(GxEPD_WHITE); + ead.drawFace(0, 100, "Calibrate", "", 11); + ead.setArm(100); + display.nextPage(); + } + else + { + updateView(); + } + break; + } } \ No newline at end of file