From 0ddc7fd28d409da918bbab409db8915f63d158ce Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 19 Dec 2023 17:26:24 +0100 Subject: [PATCH] Feature: Added diagram to display This is based on the idea of @Henrik-Ingenieur and was discussed in #1504 --- include/Configuration.h | 1 + include/Display_Graphic.h | 4 + include/Display_Graphic_Diagram.h | 39 ++++++++++ include/defaults.h | 1 + src/Configuration.cpp | 2 + src/Display_Graphic.cpp | 11 ++- src/Display_Graphic_Diagram.cpp | 105 +++++++++++++++++++++++++++ src/WebApi_device.cpp | 4 +- webapp/src/locales/de.json | 3 + webapp/src/locales/en.json | 3 + webapp/src/locales/fr.json | 3 + webapp/src/types/DeviceConfig.ts | 1 + webapp/src/views/DeviceAdminView.vue | 5 ++ 13 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 include/Display_Graphic_Diagram.h create mode 100644 src/Display_Graphic_Diagram.cpp diff --git a/include/Configuration.h b/include/Configuration.h index 702e9f4..d355f46 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -143,6 +143,7 @@ struct CONFIG_T { uint8_t Rotation; uint8_t Contrast; uint8_t Language; + uint32_t DiagramDuration; } Display; struct { diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index 7c71c55..1d620e5 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Display_Graphic_Diagram.h" #include "defaults.h" #include #include @@ -24,6 +25,8 @@ public: void setLanguage(const uint8_t language); void setStartupDisplay(); + DisplayGraphicDiagramClass& Diagram(); + bool enablePowerSafe = true; bool enableScreensaver = true; @@ -36,6 +39,7 @@ private: Task _loopTask; U8G2* _display; + DisplayGraphicDiagramClass _diagram; bool _displayTurnedOn; diff --git a/include/Display_Graphic_Diagram.h b/include/Display_Graphic_Diagram.h new file mode 100644 index 0000000..0626436 --- /dev/null +++ b/include/Display_Graphic_Diagram.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +#define CHART_HEIGHT 20 // chart area hight in pixels +#define CHART_WIDTH 47 // chart area width in pixels +#define DIAG_POSX 80 // position were Diag is drawn at +#define DIAG_POSY 0 + +class DisplayGraphicDiagramClass { +public: + DisplayGraphicDiagramClass(); + + void init(Scheduler& scheduler, U8G2* display); + void redraw(); + + void updatePeriod(); + +private: + void averageLoop(); + void dataPointLoop(); + + static uint32_t getSecondsPerDot(); + + Task _averageTask; + Task _dataPointTask; + + U8G2* _display = nullptr; + std::array _graphValues = {}; + uint8_t _graphValuesCount = 0; + + float _iRunningAverage = 0; + uint16_t _iRunningAverageCnt = 0; + + uint8_t _graphPosX = DIAG_POSX; +}; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index 0bbf721..e783df2 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -98,6 +98,7 @@ #define DISPLAY_ROTATION 2U #define DISPLAY_CONTRAST 60U #define DISPLAY_LANGUAGE 0U +#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL) #define REACHABLE_THRESHOLD 2U diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 89402fc..d1ff8fe 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -103,6 +103,7 @@ bool ConfigurationClass::write() display["rotation"] = config.Display.Rotation; display["contrast"] = config.Display.Contrast; display["language"] = config.Display.Language; + display["diagram_duration"] = config.Display.DiagramDuration; JsonArray leds = device.createNestedArray("led"); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { @@ -264,6 +265,7 @@ bool ConfigurationClass::read() config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION; config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; config.Display.Language = display["language"] | DISPLAY_LANGUAGE; + config.Display.DiagramDuration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION; JsonArray leds = device["led"]; for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 89fcbc4..5a06452 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -51,6 +51,7 @@ void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, c _display->begin(); setContrast(DISPLAY_CONTRAST); setStatus(true); + _diagram.init(scheduler, _display); } scheduler.addTask(_loopTask); @@ -91,7 +92,7 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line) if (!_isLarge) { dispX = (line == 0) ? 5 : 0; } else { - dispX = (line == 0) ? 20 : 5; + dispX = (line == 0) ? 10 : 5; } setFont(line); @@ -140,6 +141,11 @@ void DisplayGraphicClass::setStartupDisplay() _display->sendBuffer(); } +DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram() +{ + return _diagram; +} + void DisplayGraphicClass::loop() { if (_display_type == DisplayType_t::None) { @@ -154,6 +160,9 @@ void DisplayGraphicClass::loop() //=====> Actual Production ========== if (Datastore.getIsAtLeastOneReachable()) { displayPowerSave = false; + if (_isLarge) { + _diagram.redraw(); + } if (Datastore.getTotalAcPowerEnabled() > 999) { snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); } else { diff --git a/src/Display_Graphic_Diagram.cpp b/src/Display_Graphic_Diagram.cpp new file mode 100644 index 0000000..4a98c16 --- /dev/null +++ b/src/Display_Graphic_Diagram.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "Display_Graphic_Diagram.h" +#include "Configuration.h" +#include "Datastore.h" +#include + +DisplayGraphicDiagramClass::DisplayGraphicDiagramClass() +{ +} + +void DisplayGraphicDiagramClass::init(Scheduler& scheduler, U8G2* display) +{ + _display = display; + + scheduler.addTask(_averageTask); + _averageTask.setCallback(std::bind(&DisplayGraphicDiagramClass::averageLoop, this)); + _averageTask.setIterations(TASK_FOREVER); + _averageTask.setInterval(1 * TASK_SECOND); + _averageTask.enable(); + + scheduler.addTask(_dataPointTask); + _dataPointTask.setCallback(std::bind(&DisplayGraphicDiagramClass::dataPointLoop, this)); + _dataPointTask.setIterations(TASK_FOREVER); + updatePeriod(); + _dataPointTask.enable(); +} + +void DisplayGraphicDiagramClass::averageLoop() +{ + const float currentWatts = Datastore.getTotalAcPowerEnabled(); // get the current AC production + _iRunningAverage += currentWatts; + _iRunningAverageCnt++; +} + +void DisplayGraphicDiagramClass::dataPointLoop() +{ + if (_graphValuesCount >= CHART_WIDTH) { + for (uint8_t i = 0; i < CHART_WIDTH - 1; i++) { + _graphValues[i] = _graphValues[i + 1]; + } + _graphValuesCount = CHART_WIDTH - 1; + } + if (_iRunningAverageCnt != 0) { + _graphValues[_graphValuesCount++] = _iRunningAverage / _iRunningAverageCnt; + _iRunningAverage = 0; + _iRunningAverageCnt = 0; + } + + if (Configuration.get().Display.ScreenSaver) { + _graphPosX = DIAG_POSX - (_graphValuesCount % 2); + } +} + +uint32_t DisplayGraphicDiagramClass::getSecondsPerDot() +{ + return Configuration.get().Display.DiagramDuration / CHART_WIDTH; +} + +void DisplayGraphicDiagramClass::updatePeriod() +{ + _dataPointTask.setInterval(getSecondsPerDot() * TASK_SECOND); +} + +void DisplayGraphicDiagramClass::redraw() +{ + uint8_t graphPosY = DIAG_POSY; + + // draw diagram axis + _display->drawVLine(_graphPosX, graphPosY, CHART_HEIGHT); + _display->drawHLine(_graphPosX, graphPosY + CHART_HEIGHT - 1, CHART_WIDTH); + + _display->drawLine(_graphPosX + 1, graphPosY + 1, _graphPosX + 2, graphPosY + 2); // UP-arrow + _display->drawLine(_graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT - 3, _graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT - 2); // LEFT-arrow + _display->drawLine(_graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT + 1, _graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT); // LEFT-arrow + + // draw AC value + _display->setFont(u8g2_font_tom_thumb_4x6_mr); + char fmtText[7]; + const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end()); + snprintf(fmtText, sizeof(fmtText), "%dW", static_cast(maxWatts)); + const uint8_t textLength = strlen(fmtText); + _display->drawStr(_graphPosX - (textLength * 4), graphPosY + 5, fmtText); + + // draw chart + const float scaleFactor = maxWatts / CHART_HEIGHT; + uint8_t axisTick = 1; + for (int i = 0; i < _graphValuesCount; i++) { + if (scaleFactor > 0) { + if (i == 0) { + _display->drawPixel(_graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); // + 0.5 to round mathematical + } else { + _display->drawLine(_graphPosX + i, graphPosY + CHART_HEIGHT - ((_graphValues[i - 1] / scaleFactor) + 0.5), _graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); + } + } + + // draw one tick per hour to the x-axis + if (i * getSecondsPerDot() > (3600u * axisTick)) { + _display->drawPixel(_graphPosX + 1 + i, graphPosY + CHART_HEIGHT); + axisTick++; + } + } +} diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 9b7de5c..f8f93d9 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -83,6 +83,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) display["screensaver"] = config.Display.ScreenSaver; display["contrast"] = config.Display.Contrast; display["language"] = config.Display.Language; + display["diagramduration"] = config.Display.DiagramDuration; JsonArray leds = root.createNestedArray("led"); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { @@ -160,9 +161,9 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) config.Display.ScreenSaver = root["display"]["screensaver"].as(); config.Display.Contrast = root["display"]["contrast"].as(); config.Display.Language = root["display"]["language"].as(); + config.Display.DiagramDuration = root["display"]["diagramduration"].as(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - Serial.println(root["led"][i]["brightness"].as()); config.Led_Single[i].Brightness = root["led"][i]["brightness"].as(); config.Led_Single[i].Brightness = min(100, config.Led_Single[i].Brightness); } @@ -172,6 +173,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) Display.enableScreensaver = config.Display.ScreenSaver; Display.setContrast(config.Display.Contrast); Display.setLanguage(config.Display.Language); + Display.Diagram().updatePeriod(); Configuration.write(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 5018ff4..164c8fd 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -571,6 +571,9 @@ "PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", "Screensaver": "Bildschirmschoner aktivieren:", "ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)", + "DiagramDuration": "Diagramm Periode:", + "DiagramDurationHint": "Die Zeitperiode welche im Diagramm dargestellt wird.", + "Seconds": "Sekunden", "Contrast": "Kontrast ({contrast}):", "Rotation": "Rotation:", "rot0": "Keine Rotation", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index b8a6eb0..7c6a74f 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -571,6 +571,9 @@ "PowerSafeHint": "Turn off the display if no inverter is producing.", "Screensaver": "Enable Screensaver:", "ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)", + "DiagramDuration": "Diagram duration:", + "DiagramDurationHint": "The time period which is shown in the diagram.", + "Seconds": "Seconds", "Contrast": "Contrast ({contrast}):", "Rotation": "Rotation:", "rot0": "No rotation", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 30abfc6..dde76f0 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -573,6 +573,9 @@ "PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.", "Screensaver": "Activer l'écran de veille", "ScreensaverHint": "Déplacez un peu l'écran à chaque mise à jour pour éviter le phénomène de brûlure. (Utile surtout pour les écrans OLED)", + "DiagramDuration": "Diagram duration:", + "DiagramDurationHint": "The time period which is shown in the diagram.", + "Seconds": "Seconds", "Contrast": "Contraste ({contrast}):", "Rotation": "Rotation:", "rot0": "Pas de rotation", diff --git a/webapp/src/types/DeviceConfig.ts b/webapp/src/types/DeviceConfig.ts index 0711526..27b4450 100644 --- a/webapp/src/types/DeviceConfig.ts +++ b/webapp/src/types/DeviceConfig.ts @@ -6,6 +6,7 @@ export interface Display { screensaver: boolean; contrast: number; language: number; + diagramduration: number; } export interface Led { diff --git a/webapp/src/views/DeviceAdminView.vue b/webapp/src/views/DeviceAdminView.vue index f340c56..1f35099 100644 --- a/webapp/src/views/DeviceAdminView.vue +++ b/webapp/src/views/DeviceAdminView.vue @@ -67,6 +67,11 @@ v-model="deviceConfigList.display.screensaver" type="checkbox" :tooltip="$t('deviceadmin.ScreensaverHint')" /> + +