Feature: Added diagram to display

This is based on the idea of @Henrik-Ingenieur and was discussed in #1504
This commit is contained in:
Thomas Basler 2023-12-19 17:26:24 +01:00
parent 3b923885de
commit 0ddc7fd28d
13 changed files with 180 additions and 2 deletions

View File

@ -143,6 +143,7 @@ struct CONFIG_T {
uint8_t Rotation; uint8_t Rotation;
uint8_t Contrast; uint8_t Contrast;
uint8_t Language; uint8_t Language;
uint32_t DiagramDuration;
} Display; } Display;
struct { struct {

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "Display_Graphic_Diagram.h"
#include "defaults.h" #include "defaults.h"
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <U8g2lib.h> #include <U8g2lib.h>
@ -24,6 +25,8 @@ public:
void setLanguage(const uint8_t language); void setLanguage(const uint8_t language);
void setStartupDisplay(); void setStartupDisplay();
DisplayGraphicDiagramClass& Diagram();
bool enablePowerSafe = true; bool enablePowerSafe = true;
bool enableScreensaver = true; bool enableScreensaver = true;
@ -36,6 +39,7 @@ private:
Task _loopTask; Task _loopTask;
U8G2* _display; U8G2* _display;
DisplayGraphicDiagramClass _diagram;
bool _displayTurnedOn; bool _displayTurnedOn;

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <TaskSchedulerDeclarations.h>
#include <U8g2lib.h>
#include <array>
#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<float, CHART_WIDTH> _graphValues = {};
uint8_t _graphValuesCount = 0;
float _iRunningAverage = 0;
uint16_t _iRunningAverageCnt = 0;
uint8_t _graphPosX = DIAG_POSX;
};

View File

@ -98,6 +98,7 @@
#define DISPLAY_ROTATION 2U #define DISPLAY_ROTATION 2U
#define DISPLAY_CONTRAST 60U #define DISPLAY_CONTRAST 60U
#define DISPLAY_LANGUAGE 0U #define DISPLAY_LANGUAGE 0U
#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL)
#define REACHABLE_THRESHOLD 2U #define REACHABLE_THRESHOLD 2U

View File

@ -103,6 +103,7 @@ bool ConfigurationClass::write()
display["rotation"] = config.Display.Rotation; display["rotation"] = config.Display.Rotation;
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["language"] = config.Display.Language;
display["diagram_duration"] = config.Display.DiagramDuration;
JsonArray leds = device.createNestedArray("led"); JsonArray leds = device.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { 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.Rotation = display["rotation"] | DISPLAY_ROTATION;
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
config.Display.Language = display["language"] | DISPLAY_LANGUAGE; config.Display.Language = display["language"] | DISPLAY_LANGUAGE;
config.Display.DiagramDuration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
JsonArray leds = device["led"]; JsonArray leds = device["led"];
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {

View File

@ -51,6 +51,7 @@ void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, c
_display->begin(); _display->begin();
setContrast(DISPLAY_CONTRAST); setContrast(DISPLAY_CONTRAST);
setStatus(true); setStatus(true);
_diagram.init(scheduler, _display);
} }
scheduler.addTask(_loopTask); scheduler.addTask(_loopTask);
@ -91,7 +92,7 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line)
if (!_isLarge) { if (!_isLarge) {
dispX = (line == 0) ? 5 : 0; dispX = (line == 0) ? 5 : 0;
} else { } else {
dispX = (line == 0) ? 20 : 5; dispX = (line == 0) ? 10 : 5;
} }
setFont(line); setFont(line);
@ -140,6 +141,11 @@ void DisplayGraphicClass::setStartupDisplay()
_display->sendBuffer(); _display->sendBuffer();
} }
DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram()
{
return _diagram;
}
void DisplayGraphicClass::loop() void DisplayGraphicClass::loop()
{ {
if (_display_type == DisplayType_t::None) { if (_display_type == DisplayType_t::None) {
@ -154,6 +160,9 @@ void DisplayGraphicClass::loop()
//=====> Actual Production ========== //=====> Actual Production ==========
if (Datastore.getIsAtLeastOneReachable()) { if (Datastore.getIsAtLeastOneReachable()) {
displayPowerSave = false; displayPowerSave = false;
if (_isLarge) {
_diagram.redraw();
}
if (Datastore.getTotalAcPowerEnabled() > 999) { if (Datastore.getTotalAcPowerEnabled() > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000));
} else { } else {

View File

@ -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 <algorithm>
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<uint16_t>(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++;
}
}
}

View File

@ -83,6 +83,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["screensaver"] = config.Display.ScreenSaver; display["screensaver"] = config.Display.ScreenSaver;
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["language"] = config.Display.Language;
display["diagramduration"] = config.Display.DiagramDuration;
JsonArray leds = root.createNestedArray("led"); JsonArray leds = root.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { 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<bool>(); config.Display.ScreenSaver = root["display"]["screensaver"].as<bool>();
config.Display.Contrast = root["display"]["contrast"].as<uint8_t>(); config.Display.Contrast = root["display"]["contrast"].as<uint8_t>();
config.Display.Language = root["display"]["language"].as<uint8_t>(); config.Display.Language = root["display"]["language"].as<uint8_t>();
config.Display.DiagramDuration = root["display"]["diagramduration"].as<uint32_t>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
Serial.println(root["led"][i]["brightness"].as<uint8_t>());
config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>(); config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>();
config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness); config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness);
} }
@ -172,6 +173,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
Display.enableScreensaver = config.Display.ScreenSaver; Display.enableScreensaver = config.Display.ScreenSaver;
Display.setContrast(config.Display.Contrast); Display.setContrast(config.Display.Contrast);
Display.setLanguage(config.Display.Language); Display.setLanguage(config.Display.Language);
Display.Diagram().updatePeriod();
Configuration.write(); Configuration.write();

View File

@ -571,6 +571,9 @@
"PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", "PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt",
"Screensaver": "Bildschirmschoner aktivieren:", "Screensaver": "Bildschirmschoner aktivieren:",
"ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)", "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}):", "Contrast": "Kontrast ({contrast}):",
"Rotation": "Rotation:", "Rotation": "Rotation:",
"rot0": "Keine Rotation", "rot0": "Keine Rotation",

View File

@ -571,6 +571,9 @@
"PowerSafeHint": "Turn off the display if no inverter is producing.", "PowerSafeHint": "Turn off the display if no inverter is producing.",
"Screensaver": "Enable Screensaver:", "Screensaver": "Enable Screensaver:",
"ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)", "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}):", "Contrast": "Contrast ({contrast}):",
"Rotation": "Rotation:", "Rotation": "Rotation:",
"rot0": "No rotation", "rot0": "No rotation",

View File

@ -573,6 +573,9 @@
"PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.", "PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.",
"Screensaver": "Activer l'écran de veille", "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)", "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}):", "Contrast": "Contraste ({contrast}):",
"Rotation": "Rotation:", "Rotation": "Rotation:",
"rot0": "Pas de rotation", "rot0": "Pas de rotation",

View File

@ -6,6 +6,7 @@ export interface Display {
screensaver: boolean; screensaver: boolean;
contrast: number; contrast: number;
language: number; language: number;
diagramduration: number;
} }
export interface Led { export interface Led {

View File

@ -67,6 +67,11 @@
v-model="deviceConfigList.display.screensaver" type="checkbox" v-model="deviceConfigList.display.screensaver" type="checkbox"
:tooltip="$t('deviceadmin.ScreensaverHint')" /> :tooltip="$t('deviceadmin.ScreensaverHint')" />
<InputElement :label="$t('deviceadmin.DiagramDuration')"
v-model="deviceConfigList.display.diagramduration" type="number"
min=600 max=86400
:tooltip="$t('deviceadmin.DiagramDurationHint')" :postfix="$t('deviceadmin.Seconds')" />
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-2 col-form-label"> <label class="col-sm-2 col-form-label">
{{ $t('deviceadmin.DisplayLanguage') }} {{ $t('deviceadmin.DisplayLanguage') }}