Feature: Added diagram to display
This is based on the idea of @Henrik-Ingenieur and was discussed in #1504
This commit is contained in:
parent
3b923885de
commit
0ddc7fd28d
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
39
include/Display_Graphic_Diagram.h
Normal file
39
include/Display_Graphic_Diagram.h
Normal 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;
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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++) {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
105
src/Display_Graphic_Diagram.cpp
Normal file
105
src/Display_Graphic_Diagram.cpp
Normal 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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') }}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user