diff --git a/platformio.ini b/platformio.ini index d0a3537..beee78b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,6 +13,7 @@ platform = espressif32 board = esp32dev framework = arduino lib_deps = https://github.com/adafruit/Adafruit_NeoPixel + https://github.com/knolleary/pubsubclient build_flags = upload_port = 10.0.0.116 upload_protocol = espota diff --git a/src/display/Color.cpp b/src/display/Color.cpp index 6176f7d..3be724d 100644 --- a/src/display/Color.cpp +++ b/src/display/Color.cpp @@ -8,6 +8,8 @@ const Color RED = {FULL, ____, ____}; const Color GREEN = {____, FULL, ____}; +const Color ORANGE = {FULL, HALF, ____}; + const Color BLUE = {____, ____, FULL}; const Color YELLOW = {FULL, FULL, ____}; diff --git a/src/display/Color.h b/src/display/Color.h index a44b4f8..280fe8b 100644 --- a/src/display/Color.h +++ b/src/display/Color.h @@ -21,6 +21,8 @@ extern const Color RED; extern const Color GREEN; +extern const Color ORANGE; + extern const Color BLUE; extern const Color YELLOW; diff --git a/src/display/Display.h b/src/display/Display.h index f1c2ba5..2b4cb1f 100644 --- a/src/display/Display.h +++ b/src/display/Display.h @@ -6,6 +6,7 @@ #include "Vector.h" #define SYMBOL_COUNT 15 +#define SYMBOL_DASH 13 #define DISPLAY_CHAR_WIDTH 3 #define DISPLAY_CHAR_HEIGHT 5 @@ -89,7 +90,10 @@ public: memset(buffer, 0, pixelByteCount); } - uint8_t print(uint8_t xPos, uint8_t yPos, uint8_t index, Color color) { + uint8_t print(uint8_t xPos, uint8_t yPos, uint8_t index, Color color, bool showIfZero) { + if (index == 0 && !showIfZero) { + return DISPLAY_CHAR_WIDTH; + } if (index >= SYMBOL_COUNT) { Serial.printf("Cannot print symbol #%u.\n", index); index = SYMBOL_COUNT - 1; diff --git a/src/main.cpp b/src/main.cpp index 2d03460..6ae0c79 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "mode.h" #include "display.h" +#include "mqtt.h" void setup() { delay(500); @@ -23,6 +24,7 @@ void setup() { void loop() { serial_loop(); wifi_loop(); + mqtt_loop(); server_loop(); mode_loop(); config_loop(); diff --git a/src/mode.cpp b/src/mode.cpp index 3b044ec..88e4a3c 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -9,6 +9,7 @@ #include "mode/Starfield/Starfield.h" #include "mode/Matrix/Matrix.h" #include "display.h" +#include "mode/Electricity/Electricity.h" ModeId currentModeId = NONE; @@ -108,6 +109,9 @@ void loadNewMode() { case MATRIX: mode = new Matrix(display); break; + case ELECTRICITY: + mode = new Electricity(display); + break; default: Serial.print("No mode loaded.\n"); display.clear(); diff --git a/src/mode/Clock/Clock.h b/src/mode/Clock/Clock.h index 30b1587..ac9c18c 100644 --- a/src/mode/Clock/Clock.h +++ b/src/mode/Clock/Clock.h @@ -28,17 +28,17 @@ protected: display.clear(); uint8_t x = 2; - x += display.print(x, 1, realtimeOK ? now.tm_hour / 10 : 13, WHITE); + x += display.print(x, 1, realtimeOK ? now.tm_hour / 10 : SYMBOL_DASH, WHITE, true); x++; - x += display.print(x, 1, realtimeOK ? now.tm_hour % 10 : 13, WHITE); - x += display.print(x, 1, 10, WHITE); - x += display.print(x, 1, realtimeOK ? now.tm_min / 10 : 13, WHITE); + x += display.print(x, 1, realtimeOK ? now.tm_hour % 10 : SYMBOL_DASH, WHITE, true); + x += display.print(x, 1, 10, WHITE, true); + x += display.print(x, 1, realtimeOK ? now.tm_min / 10 : SYMBOL_DASH, WHITE, true); x++; - x += display.print(x, 1, realtimeOK ? now.tm_min % 10 : 13, WHITE); - x += display.print(x, 1, 10, WHITE); - x += display.print(x, 1, realtimeOK ? now.tm_sec / 10 : 13, WHITE); + x += display.print(x, 1, realtimeOK ? now.tm_min % 10 : SYMBOL_DASH, WHITE, true); + x += display.print(x, 1, 10, WHITE, true); + x += display.print(x, 1, realtimeOK ? now.tm_sec / 10 : SYMBOL_DASH, WHITE, true); x++; - x += display.print(x, 1, realtimeOK ? now.tm_sec % 10 : 13, WHITE); + x += display.print(x, 1, realtimeOK ? now.tm_sec % 10 : SYMBOL_DASH, WHITE, true); } }; diff --git a/src/mode/CountDown/CountDown.h b/src/mode/CountDown/CountDown.h index 6f66e05..329b234 100644 --- a/src/mode/CountDown/CountDown.h +++ b/src/mode/CountDown/CountDown.h @@ -151,15 +151,15 @@ private: uint8_t x = 0; if (days > 0) { drawDay(display, days, &x); - x += display.print(x, 1, 10, WHITE); + x += display.print(x, 1, 10, WHITE, true); } else { x += 2; } drawHour(display, days, hours, &x); - x += display.print(x, 1, 10, WHITE); + x += display.print(x, 1, 10, WHITE, true); draw2Digit(display, minutes, &x); if (days <= 0) { - x += display.print(x, 1, 10, WHITE); + x += display.print(x, 1, 10, WHITE, true); draw2Digit(display, seconds, &x); drawSubSecondsBar(display); } else { @@ -169,51 +169,51 @@ private: static void drawNoTime(Display &display) { uint8_t x = 2; - x += display.print(x, 1, 13, WHITE); + x += display.print(x, 1, SYMBOL_DASH, WHITE, true); x++; - x += display.print(x, 1, 13, WHITE); - x += display.print(x, 1, 10, WHITE); - x += display.print(x, 1, 13, WHITE); + x += display.print(x, 1, SYMBOL_DASH, WHITE, true); + x += display.print(x, 1, 10, WHITE, true); + x += display.print(x, 1, SYMBOL_DASH, WHITE, true); x++; - x += display.print(x, 1, 13, WHITE); - x += display.print(x, 1, 10, WHITE); - x += display.print(x, 1, 13, WHITE); + x += display.print(x, 1, SYMBOL_DASH, WHITE, true); + x += display.print(x, 1, 10, WHITE, true); + x += display.print(x, 1, SYMBOL_DASH, WHITE, true); x++; - x += display.print(x, 1, 13, WHITE); + x += display.print(x, 1, SYMBOL_DASH, WHITE, true); } static void drawDay(Display &display, int days, uint8_t *x) { if (days >= 100) { - *x += display.print(*x, 1, days / 100, WHITE); + *x += display.print(*x, 1, days / 100, WHITE, true); } else { *x += 3; } (*x)++; if (days >= 10) { - *x += display.print(*x, 1, days / 10 % 10, WHITE); + *x += display.print(*x, 1, days / 10 % 10, WHITE, true); } else { *x += 3; } (*x)++; - *x += display.print(*x, 1, days % 10, WHITE); + *x += display.print(*x, 1, days % 10, WHITE, true); } static void drawHour(Display &display, int days, int hours, uint8_t *x) { if (days > 0 || hours >= 10) { - *x += display.print(*x, 1, hours / 10, WHITE); + *x += display.print(*x, 1, hours / 10, WHITE, true); } else { *x += 3; } (*x)++; - *x += display.print(*x, 1, hours % 10, WHITE); + *x += display.print(*x, 1, hours % 10, WHITE, true); } static void draw2Digit(Display &display, int value, uint8_t *x) { - *x += display.print(*x, 1, value / 10, WHITE); + *x += display.print(*x, 1, value / 10, WHITE, true); (*x)++; - *x += display.print(*x, 1, value % 10, WHITE); + *x += display.print(*x, 1, value % 10, WHITE, true); } static void drawSecondsBar(Display &display, int seconds) { @@ -244,13 +244,13 @@ private: static void drawYear(Display &display, int year) { uint8_t x = 8; - x += display.print(x, 1, year / 1000 % 10, WHITE); + x += display.print(x, 1, year / 1000 % 10, WHITE, true); x++; - x += display.print(x, 1, year / 100 % 10, WHITE); + x += display.print(x, 1, year / 100 % 10, WHITE, true); x++; - x += display.print(x, 1, year / 10 % 10, WHITE); + x += display.print(x, 1, year / 10 % 10, WHITE, true); x++; - x += display.print(x, 1, year / 1 % 10, WHITE); + x += display.print(x, 1, year / 1 % 10, WHITE, true); } }; diff --git a/src/mode/Electricity/Electricity.h b/src/mode/Electricity/Electricity.h new file mode 100644 index 0000000..254f369 --- /dev/null +++ b/src/mode/Electricity/Electricity.h @@ -0,0 +1,67 @@ +#ifndef MODE_ELECTRICITY_H +#define MODE_ELECTRICITY_H + +#include "mode/Mode.h" +#include "mqtt.h" + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnusedValue" + +class Electricity : public Mode { + +public: + + explicit Electricity(Display &display) : + Mode(display) { + // nothing + } + + const char *getName() override { + return "Electricity"; + } + +protected: + + void step(microseconds_t microseconds) override { + if (realtimeChanged) { + markDirty(); + } + } + + void draw(Display &display) override { + display.clear(); + uint8_t x = 0; + + int pv = (int) round(getPhotovoltaicPowerW()); + int pv100 = (pv / 100) % 10; + int pv10 = (pv / 10) % 10; + int pv1 = pv % 10; + Serial.printf("pv100=%d, pv10=%d, pv1=%d, pv=%f\n", pv100, pv10, pv1, getPhotovoltaicPowerW()); + x += display.print(x, 1, isnan(pv) ? SYMBOL_DASH : pv100, GREEN, pv >= 100) + 1; + x += display.print(x, 1, isnan(pv) ? SYMBOL_DASH : pv10, GREEN, pv >= 10) + 1; + x += display.print(x, 1, isnan(pv) ? SYMBOL_DASH : pv1, GREEN, true) + 1; + + x += 5; + + int grid = (int) round(getGridPowerW()); + Color color = ORANGE; + if (grid < 0) { + color = MAGENTA; + grid = -grid; + } + int grid1000 = (grid / 1000) % 10; + int grid100 = (grid / 100) % 10; + int grid10 = (grid / 10) % 10; + int grid1 = grid % 10; + Serial.printf("grid100=%d, grid10=%d, grid1=%d, grid=%f\n", grid100, grid10, grid1, getGridPowerW()); + x += display.print(x, 1, isnan(grid) ? SYMBOL_DASH : grid1000, color, grid >= 1000) + 1; + x += display.print(x, 1, isnan(grid) ? SYMBOL_DASH : grid100, color, grid >= 100) + 1; + x += display.print(x, 1, isnan(grid) ? SYMBOL_DASH : grid10, color, grid >= 10) + 1; + x += display.print(x, 1, isnan(grid) ? SYMBOL_DASH : grid1, color, true) + 1; + } + +}; + +#pragma clang diagnostic pop + +#endif diff --git a/src/mode/Mode.h b/src/mode/Mode.h index afa3049..c74ffe4 100644 --- a/src/mode/Mode.h +++ b/src/mode/Mode.h @@ -23,6 +23,7 @@ enum ModeId { COUNT_DOWN_BARS, STARFIELD, MATRIX, + ELECTRICITY, }; class Mode { diff --git a/src/mode/Pong/Pong.h b/src/mode/Pong/Pong.h index 3b30ff9..cae5325 100644 --- a/src/mode/Pong/Pong.h +++ b/src/mode/Pong/Pong.h @@ -97,8 +97,8 @@ protected: display.clear(); switch (status) { case SCORE: - display.print(1, 1, player0.score, GREEN); - display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, player1.score, RED); + display.print(1, 1, player0.score, GREEN, true); + display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, player1.score, RED, true); break; case PLAY: for (int i = 0; i < player0.size; ++i) { @@ -111,11 +111,11 @@ protected: break; case OVER: if (player0.score > player1.score) { - display.print(1, 1, 11, GREEN); - display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, 12, RED); + display.print(1, 1, 11, GREEN, true); + display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, 12, RED, true); } else if (player0.score < player1.score) { - display.print(1, 1, 12, RED); - display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, 11, GREEN); + display.print(1, 1, 12, RED, true); + display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, 11, GREEN, true); } break; } diff --git a/src/mqtt.cpp b/src/mqtt.cpp new file mode 100644 index 0000000..2c2c085 --- /dev/null +++ b/src/mqtt.cpp @@ -0,0 +1,94 @@ +#include +#include +#include "mqtt.h" +#include "PubSubClient.h" +#include "wifi.h" + +#define MQTT_CONNECT_TIMEOUT_MILLISECONDS 5000 + +#define MQTT_MAX_MESSAGE_AGE_MILLIS 5000 + +#define PHOTOVOLTAIC_POWER "openDTU/pv/ac/power" + +#define GRID_POWER "electricity/grid/power/signed/w" + +WiFiClient espClient; + +PubSubClient mqtt(espClient); + +bool mqttConnected = false; + +unsigned long mqttLastConnectTry = 0; + +double photovoltaicPowerW = NAN; + +unsigned long photovoltaicPowerWLast = 0; + +double gridPowerW = NAN; + +unsigned long gridPowerWLast = 0; + +void mqttDisconnect(); + +void mqttCallback(char *topic, uint8_t *payload, unsigned int length) { + char message[128]; + if (length > sizeof message - 1) { + Serial.printf("MQTT: received too long message: topic=%s, length=%d", topic, length); + return; + } + memcpy(message, payload, length); + message[length] = 0; + if (strcmp(PHOTOVOLTAIC_POWER, topic) == 0) { + photovoltaicPowerW = strtod(message, nullptr); + photovoltaicPowerWLast = millis(); + } else if (strcmp(GRID_POWER, topic) == 0) { + gridPowerW = strtod(message, nullptr); + gridPowerWLast = millis(); + } +} + +void mqtt_loop() { + if (!wifiIsConnected()) { + mqttDisconnect(); + return; + } + if (!mqtt.loop() && (mqttLastConnectTry == 0 || millis() - mqttLastConnectTry > MQTT_CONNECT_TIMEOUT_MILLISECONDS)) { + mqttLastConnectTry = millis(); + mqtt.setServer("10.0.0.50", 1883); + mqttConnected = mqtt.connect(WiFiClass::getHostname()); + if (mqttConnected) { + Serial.printf("Successfully connected mqtt broker at %s:%d\n", "10.0.0.50", 1883); + mqtt.setCallback(mqttCallback); + mqtt.subscribe(PHOTOVOLTAIC_POWER); + mqtt.subscribe(GRID_POWER); + } else { + Serial.printf("ERROR: Failed to connect MQTT broker at %s:%d\n", "10.0.0.50", 1883); + mqttDisconnect(); + } + } +} + +void mqttDisconnect() { + if (mqttConnected) { + mqtt.disconnect(); + mqttConnected = false; + photovoltaicPowerW = NAN; + photovoltaicPowerWLast = 0; + gridPowerW = NAN; + gridPowerWLast = 0; + } +} + +double getPhotovoltaicPowerW() { + if (millis() - photovoltaicPowerWLast > MQTT_MAX_MESSAGE_AGE_MILLIS) { + return NAN; + } + return photovoltaicPowerW; +} + +double getGridPowerW() { + if (millis() - gridPowerWLast > MQTT_MAX_MESSAGE_AGE_MILLIS) { + return NAN; + } + return gridPowerW; +} diff --git a/src/mqtt.h b/src/mqtt.h new file mode 100644 index 0000000..485d326 --- /dev/null +++ b/src/mqtt.h @@ -0,0 +1,10 @@ +#ifndef SENSOR2_MQTT_H +#define SENSOR2_MQTT_H + +void mqtt_loop(); + +double getPhotovoltaicPowerW(); + +double getGridPowerW(); + +#endif diff --git a/src/server.cpp b/src/server.cpp index 3f01407..74875fc 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -105,6 +105,7 @@ void web_index() { server.sendContent(R"(COUNT_DOWN_BARS
)"); server.sendContent(R"(STARFIELD
)"); server.sendContent(R"(MATRIX
)"); + server.sendContent(R"(ELECTRICITY
)"); server.sendContent(R"(

)"); server.sendContent(R"(

)"); diff --git a/src/wifi.cpp b/src/wifi.cpp index 6f7708a..bb3fff1 100644 --- a/src/wifi.cpp +++ b/src/wifi.cpp @@ -1,13 +1,14 @@ #include "wifi.h" #include "display.h" +#include "mqtt.h" #include #include #include -bool connected = false; +bool wifiConnected = false; -void ntp_setup(); +void onConnect(); uint32_t ip2int(const IPAddress &ip); @@ -16,6 +17,7 @@ void timeSyncCallback(struct timeval *tv); char *calculateGateway(char *calculatedGateway, size_t size); void wifi_setup() { + WiFiClass::setHostname("RGBMatrixDisplay"); WiFi.begin("HappyNet", "1Grausame!Sackratte7"); yield(); @@ -50,21 +52,21 @@ void wifi_setup() { void wifi_loop() { ArduinoOTA.handle(); bool hasIp = (uint32_t) WiFi.localIP() != 0; - if (!connected) { + if (!wifiConnected) { if (hasIp) { - connected = true; - Serial.printf("WiFi connected: %s\n", WiFi.localIP().toString().c_str()); - ntp_setup(); + wifiConnected = true; + onConnect(); } } else { if (!hasIp) { - connected = false; + wifiConnected = false; Serial.println("WiFi disconnected!"); } } } -void ntp_setup() { +void onConnect() { + Serial.printf("WiFi connected: %s\n", WiFi.localIP().toString().c_str()); char calculatedGateway[16] = {0}; calculateGateway(calculatedGateway, sizeof(calculatedGateway)); sntp_set_time_sync_notification_cb(timeSyncCallback); @@ -73,6 +75,10 @@ void ntp_setup() { yield(); } +bool wifiIsConnected() { + return wifiConnected; +} + char *calculateGateway(char *calculatedGateway, size_t size) { uint32_t local = ip2int(WiFi.localIP()); uint32_t netmask = ip2int(WiFi.subnetMask()); diff --git a/src/wifi.h b/src/wifi.h index 91fa910..c2bf457 100644 --- a/src/wifi.h +++ b/src/wifi.h @@ -5,4 +5,6 @@ void wifi_setup(); void wifi_loop(); +bool wifiIsConnected(); + #endif