From 6c8713eea289d67103ea1e3beaa6249b04a820b1 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Mon, 1 Sep 2025 13:55:12 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + .vscode/extensions.json | 10 ++ .vscode/settings.json | 57 ++++++++ include/README | 37 +++++ lib/README | 46 ++++++ platformio.ini | 29 ++++ src/Scripte.code-workspace | 12 ++ src/main.cpp | 277 +++++++++++++++++++++++++++++++++++++ src/settings.h | 81 +++++++++++ test/README | 11 ++ 10 files changed, 565 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 src/Scripte.code-workspace create mode 100644 src/main.cpp create mode 100644 src/settings.h create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7eb263c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,57 @@ +{ + "files.associations": { + "array": "cpp", + "deque": "cpp", + "list": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "string_view": "cpp", + "initializer_list": "cpp", + "ranges": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "map": "cpp", + "set": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "ostream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "variant": "cpp" + } +} \ No newline at end of file diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..09c8109 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,29 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:nodemcuv2] +platform = espressif8266 +board = nodemcuv2 +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 + +lib_deps = + adafruit/Adafruit GFX Library@^1.11.5 + adafruit/Adafruit ILI9341@^1.5.6 + https://github.com/PaulStoffregen/XPT2046_Touchscreen.git + bblanchon/ArduinoJson@^6.21.2 + + + + + + + diff --git a/src/Scripte.code-workspace b/src/Scripte.code-workspace new file mode 100644 index 0000000..ceeca39 --- /dev/null +++ b/src/Scripte.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": "../.." + }, + { + "name": "_lcd_test", + "path": "../../../../../../../Dokumente/PlatformIO/Projects/_lcd_test" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..1aab3a8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,277 @@ +#include +#include +#include +#include +#include +#include +#include + +// ==== Display-Pins ==== +#define TFT_CS D1 +#define TFT_DC D2 +#define TFT_RST -1 +#define TFT_LED D8 + +// ==== Touch-Pins ==== +#define TOUCH_CS D3 +#define TOUCH_IRQ D4 + +Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST); +XPT2046_Touchscreen ts(TOUCH_CS, TOUCH_IRQ); + +int meterColorMode = 1; // oben +int plugColorMode = 1; // unten + +int displayWidth = 315; // rechts 5 Pixel auslassen. Sieht besser aus im Gehäuse +int displayHight = 240; + +// ==== Farben pro Modus ==== +uint16_t meterColorPowerPositive; +uint16_t meterColorPowerNegative; +uint16_t plugColorPowerPositive; +uint16_t plugColorPowerNegative; + +void applyColorMode(int meterMode, int plugMode) { + switch (meterMode) { + case 1: meterColorPowerPositive = ILI9341_WHITE; meterColorPowerNegative = ILI9341_GREEN; break; + case 2: meterColorPowerPositive = ILI9341_RED; meterColorPowerNegative = ILI9341_GREEN; break; + case 3: meterColorPowerPositive = ILI9341_WHITE; meterColorPowerNegative = ILI9341_WHITE; break; + } + + switch (plugMode) { + case 1: plugColorPowerPositive = ILI9341_WHITE; plugColorPowerNegative = ILI9341_GREEN; break; + case 2: plugColorPowerPositive = ILI9341_YELLOW; plugColorPowerNegative = ILI9341_GREEN; break; + case 3: plugColorPowerPositive = ILI9341_GREEN; plugColorPowerNegative = ILI9341_GREEN; break; + } +} + + +const unsigned long refreshInterval = refreshrate_power; +String lastMeterPower = ""; +String lastPlugPower = ""; +bool meterWasNegative = false; +bool plugWasNegative = false; +unsigned long lastUpdateTime = 0; + +String getTasmotaValue(const String& ip, const String& jsonKey, int decimalPlaces = 0) { + WiFiClient client; + HTTPClient http; + String result = "?"; + + if (http.begin(client, "http://" + ip + "/cm?cmnd=Status%208")) { + int httpCode = http.GET(); + if (httpCode == 200) { + String response = http.getString(); + StaticJsonDocument<768> doc; + DeserializationError error = deserializeJson(doc, response); + if (!error) { + JsonObject sns = doc["StatusSNS"]; + for (JsonPair kv : sns) { + if (kv.value().is()) { + JsonObject inner = kv.value().as(); + if (inner.containsKey(jsonKey)) { + float val = inner[jsonKey]; + result = String((int)val); + break; + } + } + } + } + } + http.end(); + } + + return result; +} + +String rightAlign(String value, size_t totalLength) { + while (value.length() < totalLength) value = " " + value; + return value; +} + +void drawCenteredLabel(const char* text, int y, int textSizeLabel = 3) { + int len = strlen(text); + int textWidth = len * 6 * textSizeLabel; + int textHeight = 8 * textSizeLabel; + int x = (displayWidth - textWidth) / 2; + int rectHeight = textHeight + 4; + + tft.fillRect(0, y - 2, displayWidth, rectHeight, ILI9341_WHITE); + tft.setTextSize(textSizeLabel); + tft.setTextColor(ILI9341_BLACK, ILI9341_WHITE); + tft.setCursor(x, y); + tft.print(text); +} + +void drawRightAlignedValue(const String& newValRaw, String& oldValRaw, int startY, bool isNegativeNow, bool& wasNegativeLastTime, uint16_t colorIfPositive, uint16_t colorIfNegative) { + const int textSizeValue = 8; + const int textSizeW = 3; + const int charWidthValue = 6 * textSizeValue; + const int charWidthW = 6 * textSizeW; + const int valueHeight = 8 * textSizeValue; + const size_t maxLen = 6; + + String newVal = rightAlign(newValRaw, maxLen); + String oldVal = rightAlign(oldValRaw, maxLen); + + uint16_t color = isNegativeNow ? colorIfNegative : colorIfPositive; + bool fullRedraw = (isNegativeNow != wasNegativeLastTime); + + for (size_t i = 0; i < maxLen; i++) { + char oldChar = oldVal.charAt(i); + char newChar = newVal.charAt(i); + + if (fullRedraw || oldChar != newChar) { + int charX = displayWidth - ((maxLen - i) * charWidthValue + charWidthW); + tft.fillRect(charX, startY, charWidthValue, valueHeight, ILI9341_BLACK); + tft.setCursor(charX, startY); + tft.setTextSize(textSizeValue); + tft.setTextColor(color, ILI9341_BLACK); + tft.print(newChar); + } + } + + int wX = displayWidth - charWidthW; + tft.setTextSize(textSizeW); + tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK); + tft.setCursor(wX, startY + 20); + tft.print("W"); + + oldValRaw = newValRaw; + wasNegativeLastTime = isNegativeNow; +} + + +void showColorPreview() { + tft.fillRect(0, 0, tft.width(), tft.height(), ILI9341_BLACK); + + // Farbkästchen oben (Netzbezug) + int centerY_top = 30; + int boxSize = 30; + int centerX = tft.width() / 2; + tft.fillRect(centerX - boxSize - 5, centerY_top, boxSize, boxSize, meterColorPowerPositive); // positiv + tft.fillRect(centerX + 5, centerY_top, boxSize, boxSize, meterColorPowerNegative); // negativ + + // Farbkästchen unten (Erzeugung) + int centerY_bottom = tft.height() - 50; + tft.fillRect(centerX - boxSize - 5, centerY_bottom, boxSize, boxSize, plugColorPowerPositive); // positiv + tft.fillRect(centerX + 5, centerY_bottom, boxSize, boxSize, plugColorPowerNegative); // negativ + + delay(1000); // 1 Sekunde anzeigen +} + + + +void updateDisplay(const String& meterPower, const String& plugPower) { + bool isMeterNegative = meterPower.toFloat() < 0; + if (meterPower != lastMeterPower || isMeterNegative != meterWasNegative) { + drawRightAlignedValue(meterPower, lastMeterPower, 45, isMeterNegative, meterWasNegative, meterColorPowerPositive, meterColorPowerNegative); + } + + bool isPlugNegative = plugPower.toFloat() < 0; + if (plugPower != lastPlugPower || isPlugNegative != plugWasNegative) { + drawRightAlignedValue(plugPower, lastPlugPower, 163, isPlugNegative, plugWasNegative, plugColorPowerPositive, plugColorPowerNegative); + } +} + +void showConnectingAnimation() { + tft.fillScreen(ILI9341_BLACK); + + drawCenteredLabel("WLAN verbinden", 2); + drawCenteredLabel("Bitte warten...", 212); + + int centerX = 160; + int centerY = 110; + int delayMs = 250; + + for (int i = 0; i < 3; i++) { + for (int r = 5; r <= 30; r += 5) { + tft.drawCircle(centerX, centerY, r, ILI9341_WHITE); + delay(delayMs); + } + for (int r = 5; r <= 30; r += 5) { + tft.drawCircle(centerX, centerY, r, ILI9341_BLACK); + } + } +} + +void connectToWiFi() { + showConnectingAnimation(); + WiFi.mode(WIFI_STA); + WiFi.setHostname(hostname); + + int numNetworks = sizeof(ssidList) / sizeof(ssidList[0]); + for (int i = 0; i < numNetworks; i++) { + WiFi.begin(ssidList[i], passwordList[i]); + unsigned long startAttemptTime = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 10000) { + delay(500); + } + if (WiFi.status() == WL_CONNECTED) return; + } +} + +void resetScreen() { + tft.fillScreen(ILI9341_BLACK); + drawCenteredLabel("Netzbezug:", 2); + drawCenteredLabel("Erzeugung:", 120); + lastMeterPower = ""; + lastPlugPower = ""; +} + +void setup() { + Serial.begin(115200); + pinMode(TFT_LED, OUTPUT); + digitalWrite(TFT_LED, HIGH); + + tft.begin(); + tft.setRotation(3); + tft.fillScreen(ILI9341_BLACK); + + ts.begin(); + ts.setRotation(3); + + connectToWiFi(); + + applyColorMode(meterColorMode, plugColorMode); + + resetScreen(); +} + +void loop() { + if (ts.touched()) { + TS_Point p = ts.getPoint(); + delay(200); // Debounce + + // Touch-Koordinaten korrekt umrechnen + int touchX = map(p.x, 3900, 250, 0, tft.width()); + int touchY = map(p.y, 3700, 370, 0, tft.height()); + + if (touchY < tft.height() / 2) { + // Oben berührt → Netzbezug-Modus umschalten + meterColorMode = (meterColorMode % 3) + 1; + Serial.print("Touch oben erkannt. Neuer meterColorMode: "); + Serial.println(meterColorMode); + } else { + // Unten berührt → Erzeugung-Modus umschalten + plugColorMode = (plugColorMode % 3) + 1; + Serial.print("Touch unten erkannt. Neuer plugColorMode: "); + Serial.println(plugColorMode); + } + + applyColorMode(meterColorMode, plugColorMode); + resetScreen(); // neu zeichnen + } + + if (millis() - lastUpdateTime >= refreshInterval) { + lastUpdateTime = millis(); + if (WiFi.status() != WL_CONNECTED) { + connectToWiFi(); + return; + } + + String meterPower = getTasmotaValue(tasmota_smr_ip, tasmota_smr_json_prefix_power); + String plugPower = getTasmotaValue(tasmota_plug_ip, tasmota_plug_json_prefix_power); + updateDisplay(meterPower, plugPower); + } +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..dd2ab5f --- /dev/null +++ b/src/settings.h @@ -0,0 +1,81 @@ +#include +#ifndef SETTINGS_H +#define SETTINGS_H + +// ######################## WLAN-Daten ######################## + +const char* hostname = "ESP8266_Energiemeter"; + +const char* ssidList[] = { + "FRITZ!Box 7510 DQ", // SSID 1 + "502 Bad Gateway", // SSID 2 + // Weitere SSID hier hinzufügen +}; + +const char* passwordList[] = { + "69921782362054480598", // Passwort für SSID 1 + "t86zGf$%cb6UK^3pW8DyGTxEx", // Passwort für SSID 2 + // Weitere Passwörter hier hinzufügen +}; + +// ################## Tasmota Einstellungen ################### + +// ===================== Energiemessung ======================= + + +// Zwischenstecker am Balkonkraftwerk +String tasmota_plug_ip = "192.168.178.237"; +String tasmota_plug_json_prefix_power = "Power"; +String tasmota_plug_json_prefix_energy_today = "Today"; + +// SmartMeterReader +String tasmota_smr_ip = "192.168.178.236"; +String tasmota_smr_json_prefix_power = "Power"; + + +// ================== Register-Einstellungen ================== + +// ------------- 1.8.0 ------------- +// Überschrift im Display, wenn das 1.8.0 Register angezeigt wird (LCD Display Zeile 1) +String tasmota_smr_json_prefix_1_8_0_lcd_text = "1.8.0 (HT+NT):"; +// Tasmota Script JSON Prefix für das 1.8.0 Register (Wert in Zeile 2) +String tasmota_smr_json_prefix_1_8_0 = "E_in_180"; + +// ------------- 1.8.1 ------------- +// Überschrift im Display, wenn das 1.8.1 Register angezeigt wird (LCD Display Zeile 1) +String tasmota_smr_json_prefix_1_8_1_lcd_text = "1.8.1 (NT):"; +// Tasmota Script JSON Prefix für das 1.8.1 Register (Wert in Zeile 2) +String tasmota_smr_json_prefix_1_8_1 = "E_in_181"; + +// ------------- 1.8.2 ------------- +// Überschrift im Display, wenn das 1.8.2 Register angezeigt wird (LCD Display Zeile 1) +String tasmota_smr_json_prefix_1_8_2_lcd_text = "1.8.2 (HT):"; +// Tasmota Script JSON Prefix für das 1.8.2 Register (Wert in Zeile 2) +String tasmota_smr_json_prefix_1_8_2 = "E_in_182"; + +// ------------- 2.8.0 ------------- + +// Überschrift im Display, wenn das 2.8.0 Register angezeigt wird (LCD Display Zeile 1) +String tasmota_smr_json_prefix_2_8_0_lcd_text = "2.8.0 (Einsp.):"; +// Tasmota Script JSON Prefix für das 2.8.0 Register (Wert in Zeile 2) +String tasmota_smr_json_prefix_2_8_0 = "E_out_280"; + + +// ------- Nachkommastellen -------- +// Anzahl Nachkommastellen bei den ausgelesenen Tasmota Werten +// -1 = Alle / 0 = Keine / 2 = 2 Nachkommastellen +int tasmota_decimalplaces = 2; + + +// ################# Allgemeine Einstellungen ################# + +// Aktualisierungsrate im Bildschirm aktuelle Leistung (in Millisekunden) +unsigned long refreshrate_power = 3000; + +// Aktualisierungsrate in den Bildschirmen der Verbrauchsdaten (in Millisekunden) +unsigned long refreshrate_consumption = 15000; + + +// ############################################################ + +#endif // SETTINGS_H \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html