mirror of https://github.com/lumapu/ahoy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
18 KiB
380 lines
18 KiB
//-----------------------------------------------------------------------------
|
|
// 2023 Ahoy, https://ahoydtu.de
|
|
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#pragma once
|
|
#include <U8g2lib.h>
|
|
#define DISP_DEFAULT_TIMEOUT 60 // in seconds
|
|
#define DISP_FMT_TEXT_LEN 32
|
|
#define BOTTOM_MARGIN 5
|
|
|
|
#include "defines.h"
|
|
|
|
#ifdef ESP8266
|
|
#include <ESP8266WiFi.h>
|
|
#elif defined(ESP32)
|
|
#include <WiFi.h>
|
|
#endif
|
|
#include "../../utils/helper.h"
|
|
#include "Display_data.h"
|
|
#include "../../utils/dbg.h"
|
|
#include "../../utils/timemonitor.h"
|
|
#include "../../config/settings.h"
|
|
|
|
class DisplayMono {
|
|
public:
|
|
virtual void init(DisplayData *displayData) = 0;
|
|
virtual void config(display_t *cfg) = 0;
|
|
virtual void disp(void) = 0;
|
|
|
|
// Common loop function, manages display on/off functions for powersave and screensaver with motionsensor
|
|
// can be overridden by subclasses
|
|
virtual bool loop(bool motion) {
|
|
|
|
bool dispConditions = (!mCfg->pwrSaveAtIvOffline || (mDisplayData->nrProducing > 0)) &&
|
|
((mCfg->screenSaver != 2) || motion); // screensaver 2 .. motionsensor
|
|
|
|
if (mDisplayActive) {
|
|
if (!dispConditions) {
|
|
if (mDisplayTime.isTimeout()) { // switch display off after timeout
|
|
mDisplayActive = false;
|
|
mDisplay->setPowerSave(true);
|
|
}
|
|
}
|
|
else
|
|
mDisplayTime.reStartTimeMonitor(); // keep display on
|
|
}
|
|
else {
|
|
if (dispConditions) {
|
|
mDisplayActive = true;
|
|
mDisplayTime.reStartTimeMonitor(); // switch display on
|
|
mDisplay->setPowerSave(false);
|
|
}
|
|
}
|
|
|
|
if(mLuminance != mCfg->contrast) {
|
|
mLuminance = mCfg->contrast;
|
|
mDisplay->setContrast(mLuminance);
|
|
}
|
|
|
|
return(monoMaintainDispSwitchState()); // return flag, if display content should be updated immediately
|
|
}
|
|
|
|
protected:
|
|
enum class DispSwitchState {
|
|
TEXT,
|
|
GRAPH
|
|
};
|
|
|
|
protected:
|
|
// Common initialization function to be called by subclasses
|
|
void monoInit(U8G2* display, DisplayData *displayData) {
|
|
mDisplay = display;
|
|
mDisplayData = displayData;
|
|
mDisplay->begin();
|
|
mDisplay->setPowerSave(false); // always start with display on
|
|
mDisplay->setContrast(mLuminance);
|
|
mDisplay->clearBuffer();
|
|
mDispWidth = mDisplay->getDisplayWidth();
|
|
mDispHeight = mDisplay->getDisplayHeight();
|
|
mDispSwitchTime.stopTimeMonitor();
|
|
if (100 == mCfg->graph_ratio) // if graph ratio is 100% start in graph mode
|
|
mDispSwitchState = DispSwitchState::GRAPH;
|
|
else if (mCfg->graph_ratio != 0)
|
|
mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); // start display mode change only if ratio is neither 0 nor 100
|
|
}
|
|
|
|
// pixelshift screensaver with wipe effect
|
|
void calcPixelShift(int range) {
|
|
int8_t mod = (millis() / 10000) % ((range >> 1) << 2);
|
|
mPixelshift = (1 == mCfg->screenSaver) ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0;
|
|
}
|
|
|
|
protected:
|
|
enum class PowerGraphState {
|
|
NO_TIME_SYNC,
|
|
IN_PERIOD,
|
|
WAIT_4_NEW_PERIOD,
|
|
WAIT_4_RESTART
|
|
};
|
|
|
|
// initialize power graph and allocate data buffer based on pixel width
|
|
void initPowerGraph(uint8_t width, uint8_t height) {
|
|
DBGPRINTLN(F("---- Init Power Graph ----"));
|
|
mPgWidth = width;
|
|
mPgHeight = height;
|
|
mPgData = new float[mPgWidth];
|
|
mPgState = PowerGraphState::NO_TIME_SYNC;
|
|
resetPowerGraph();
|
|
}
|
|
|
|
// add new value to power graph and maintain state engine for period times
|
|
void addPowerGraphEntry(float val) {
|
|
if (nullptr == mPgData) // power graph not initialized
|
|
return;
|
|
|
|
bool storeStartEndTimes = false;
|
|
bool store_entry = false;
|
|
switch(mPgState) {
|
|
case PowerGraphState::NO_TIME_SYNC:
|
|
if ((mDisplayData->pGraphStartTime > 0)
|
|
&& (mDisplayData->pGraphEndTime > 0) // wait until period data is available ...
|
|
&& (mDisplayData->utcTs >= mDisplayData->pGraphStartTime)
|
|
&& (mDisplayData->utcTs < mDisplayData->pGraphEndTime)) // and current time is in period
|
|
{
|
|
storeStartEndTimes = true; // period was received -> store
|
|
store_entry = true;
|
|
mPgState = PowerGraphState::IN_PERIOD;
|
|
}
|
|
break;
|
|
case PowerGraphState::IN_PERIOD:
|
|
if (mDisplayData->utcTs > mPgEndTime) // check if end of day is reached ...
|
|
mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting
|
|
else
|
|
store_entry = true;
|
|
break;
|
|
case PowerGraphState::WAIT_4_NEW_PERIOD:
|
|
if ((mPgStartTime != mDisplayData->pGraphStartTime) || (mPgEndTime != mDisplayData->pGraphEndTime)) { // wait until new time period was received ...
|
|
storeStartEndTimes = true; // and store it for next period
|
|
mPgState = PowerGraphState::WAIT_4_RESTART;
|
|
}
|
|
break;
|
|
case PowerGraphState::WAIT_4_RESTART:
|
|
if ((mDisplayData->utcTs >= mPgStartTime) && (mDisplayData->utcTs < mPgEndTime)) { // wait until current time is in period again ...
|
|
resetPowerGraph(); // then reset power graph data
|
|
store_entry = true;
|
|
mPgState = PowerGraphState::IN_PERIOD;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// store start and end times of current time period and calculate period length
|
|
if (storeStartEndTimes) {
|
|
mPgStartTime = mDisplayData->pGraphStartTime;
|
|
mPgEndTime = mDisplayData->pGraphEndTime;
|
|
mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // time period of power graph in sec for scaling of x-axis
|
|
}
|
|
|
|
// store new value to mPgData
|
|
if (store_entry) {
|
|
mPgLastTime = mDisplayData->utcTs; // time of last datapoint
|
|
mPgLastPos = std::min((uint8_t) sss2PgPos(mPgLastTime - mPgStartTime), (uint8_t) (mPgWidth - 1)); // last datapoint based on seconds since start
|
|
mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); // update current datapoint to maximum of all seen values (= envelope curve)
|
|
mPgMaxPwr = std::max(mPgMaxPwr, val); // update max value of stored data for scaling of y-axis
|
|
}
|
|
}
|
|
|
|
// plot power graph to given display offset
|
|
void plotPowerGraph(uint8_t xoff, uint8_t yoff) {
|
|
if (nullptr == mPgData) // power graph not initialized
|
|
return;
|
|
|
|
// draw axes
|
|
mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis
|
|
mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis
|
|
|
|
// do not draw as long as time is not set correctly and no data was received
|
|
if ((0 == mPgStartTime) || (0 == mPgEndTime) || (0 == mPgLastTime) || (0 == mPgLastPos) || (mPgMaxPwr < 1))
|
|
return;
|
|
|
|
// draw X scale
|
|
tmElements_t tm;
|
|
breakTime(mPgEndTime, tm);
|
|
uint8_t endHourPg = tm.Hour; // absolute last hour in diagram
|
|
breakTime(mPgLastTime, tm);
|
|
uint8_t endHour = std::min(endHourPg, tm.Hour); // last hour of current data point in scaled diagram
|
|
|
|
breakTime(mPgStartTime, tm);
|
|
tm.Hour += 1;
|
|
tm.Minute = 0;
|
|
tm.Second = 0;
|
|
for (; tm.Hour <= endHour; tm.Hour++) {
|
|
uint8_t x_pos_screen = getPowerGraphXpos(sss2PgPos((uint32_t) makeTime(tm) - mPgStartTime)); // scale horizontal axis
|
|
if (12 == tm.Hour) {
|
|
mDisplay->drawLine(xoff + x_pos_screen, yoff, xoff + x_pos_screen, yoff - 2); // mark noon
|
|
mDisplay->drawLine(xoff + x_pos_screen - 1, yoff - 1, xoff + x_pos_screen + 1, yoff - 1);
|
|
}
|
|
else
|
|
mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1);
|
|
}
|
|
|
|
// draw Y scale
|
|
uint16_t scale_y = 10;
|
|
uint32_t maxpwr_int = static_cast<uint32_t>(std::round(mPgMaxPwr));
|
|
if (maxpwr_int > 100)
|
|
scale_y = 100;
|
|
|
|
for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) {
|
|
uint8_t ypos = yoff - static_cast<uint8_t>(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis
|
|
mDisplay->drawPixel(xoff + 1, ypos);
|
|
}
|
|
|
|
// draw curve
|
|
for (uint8_t i = 1; i <= mPgLastPos; i++) {
|
|
mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphValueYpos(i - 1),
|
|
xoff + getPowerGraphXpos(i), yoff - getPowerGraphValueYpos(i));
|
|
}
|
|
|
|
// print max power value
|
|
mDisplay->setFont(u8g2_font_4x6_tr);
|
|
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast<uint16_t>(std::round(mPgMaxPwr)));
|
|
mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText);
|
|
}
|
|
|
|
private:
|
|
bool monoMaintainDispSwitchState(void) {
|
|
bool change = false;
|
|
switch(mDispSwitchState) {
|
|
case DispSwitchState::TEXT:
|
|
if (mDispSwitchTime.isTimeout()) {
|
|
mDispSwitchState = DispSwitchState::GRAPH;
|
|
mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // graph_ratio: 0-100 Gesamtperiode 15000 ms
|
|
change = true;
|
|
}
|
|
break;
|
|
case DispSwitchState::GRAPH:
|
|
if (mDispSwitchTime.isTimeout()) {
|
|
mDispSwitchState = DispSwitchState::TEXT;
|
|
mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio));
|
|
change = true;
|
|
}
|
|
break;
|
|
}
|
|
return change;
|
|
}
|
|
|
|
// reset power graph
|
|
void resetPowerGraph() {
|
|
if (mPgData != nullptr) {
|
|
mPgMaxPwr = 0.0;
|
|
mPgLastPos = 0;
|
|
mPgLastTime = 0;
|
|
for (uint8_t i = 0; i < mPgWidth; i++) {
|
|
mPgData[i] = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// get power graph datapoint index, scaled to current time period, by seconds since start
|
|
uint8_t sss2PgPos(uint seconds_since_start) {
|
|
if(mPgPeriod)
|
|
return (seconds_since_start * (mPgWidth - 1) / mPgPeriod);
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
// get X-position of power graph, scaled to lastpos, by according data point index
|
|
uint8_t getPowerGraphXpos(uint8_t p) {
|
|
if ((p <= mPgLastPos) && (mPgLastPos > 0))
|
|
return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
// get Y-position of power graph, scaled to maximum value, by according datapoint index
|
|
uint8_t getPowerGraphValueYpos(uint8_t p) {
|
|
if ((p < mPgWidth) && (mPgMaxPwr > 0))
|
|
return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
protected:
|
|
display_t *mCfg;
|
|
U8G2 *mDisplay;
|
|
DisplayData *mDisplayData;
|
|
DispSwitchState mDispSwitchState = DispSwitchState::TEXT;
|
|
|
|
uint16_t mDispWidth;
|
|
uint8_t mExtra = 0;
|
|
int8_t mPixelshift=0;
|
|
char mFmtText[DISP_FMT_TEXT_LEN];
|
|
uint8_t mLineXOffsets[5] = {0, 0, 0, 0, 0};
|
|
uint8_t mLineYOffsets[5] = {0, 0, 0, 0, 0};
|
|
|
|
uint8_t mPgWidth = 0;
|
|
|
|
private:
|
|
float *mPgData = nullptr;
|
|
uint8_t mPgHeight = 0;
|
|
float mPgMaxPwr = 0.0;
|
|
uint32_t mPgStartTime = 0;
|
|
uint32_t mPgEndTime = 0;
|
|
uint32_t mPgPeriod = 0; // seconds
|
|
uint8_t mPgLastPos = 0;
|
|
uint32_t mPgLastTime = 0;
|
|
PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
|
|
|
|
uint16_t mDispHeight = 0;
|
|
uint8_t mLuminance = 0;
|
|
|
|
TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true);
|
|
TimeMonitor mDispSwitchTime = TimeMonitor();
|
|
bool mDisplayActive = true; // always start with display on
|
|
};
|
|
|
|
/* adapted 5x8 Font for low-res displays with symbols
|
|
Symbols:
|
|
\x80 ... antenna
|
|
\x81 ... WiFi
|
|
\x82 ... suncurve
|
|
\x83 ... sum/sigma
|
|
\x84 ... antenna crossed
|
|
\x85 ... WiFi crossed
|
|
\x86 ... sun
|
|
\x87 ... moon
|
|
\x88 ... calendar/day
|
|
\x89 ... MQTT */
|
|
const uint8_t u8g2_font_5x8_symbols_ahoy[1049] U8G2_FONT_SECTION("u8g2_font_5x8_symbols_ahoy") =
|
|
"j\0\3\2\4\4\3\4\5\10\10\0\377\6\377\6\0\1\61\2b\4\0 \5\0\304\11!\7a\306"
|
|
"\212!\11\42\7\63\335\212\304\22#\16u\304\232R\222\14JePJI\2$\14u\304\252l\251m"
|
|
"I\262E\0%\10S\315\212(\351\24&\13t\304\232(i\252\64%\1'\6\61\336\212\1(\7b"
|
|
"\305\32\245))\11b\305\212(\251(\0*\13T\304\212(Q\206D\211\2+\12U\304\252\60\32\244"
|
|
"\60\2,\7\63\275\32\245\4-\6\24\324\212!.\6\42\305\212!/\10d\304\272R[\6\60\14d"
|
|
"\304\32%R\206DJ\24\0\61\10c\305\232Dj\31\62\13d\304\32%\312\22%\33\2\63\13d\304"
|
|
"\212!\212D)Q\0\64\13d\304\252H\251\14Q\226\0\65\12d\304\212A\33\245D\1\66\13d\304"
|
|
"\32%[\42)Q\0\67\13d\304\212!\213\262(\213\0\70\14d\304\32%J\224HJ\24\0\71\13"
|
|
"d\304\32%\222\222-Q\0:\10R\305\212!\32\2;\10c\275\32\243R\2<\10c\305\252\244\224"
|
|
"\25=\10\64\314\212!\34\2>\11c\305\212\254\224\224\0?\11c\305\232\246$M\0@\15\205\274*"
|
|
")\222\226DI\244\252\2A\12d\304\32%\222\206I\12B\14d\304\212%\32\222H\32\22\0C\12"
|
|
"d\304\32%\322J\211\2D\12d\304\212%r\32\22\0E\12d\304\212A[\262l\10F\12d\304"
|
|
"\212A[\262\32\0G\13d\304\32%\322\222)Q\0H\12d\304\212H\32&S\0I\10c\305\212"
|
|
"%j\31J\12d\304\232)\253\224\42\0K\13d\304\212HI\244\244S\0L\10d\304\212\254\333\20"
|
|
"M\12d\304\212h\70D\246\0N\12d\304\212h\31\226I\12O\12d\304\32%rJ\24\0P\13"
|
|
"d\304\212%\222\206$\313\0Q\12t\274\32%\222\26\307\0R\13d\304\212%\222\206$\222\2S\14"
|
|
"d\304\32%J\302$J\24\0T\11e\304\212A\12;\1U\11d\304\212\310S\242\0V\12d\304"
|
|
"\212\310)\221\24\0W\12d\304\212\310\64\34\242\0X\13d\304\212HJ$%\222\2Y\12e\304\212"
|
|
"LKja\11Z\12d\304\212!\213\332\206\0[\10c\305\212!j\32\134\10d\304\212,l\13]"
|
|
"\10c\305\212\251i\10^\6#\345\232\6_\6\24\274\212!`\6\42\345\212(a\11D\304\232!\222"
|
|
"\222\1b\13d\304\212,[\42iH\0c\7C\305\232)\23d\12d\304\272\312\20I\311\0e\11"
|
|
"D\304\32%\31\262\1f\12d\304\252Ji\312\42\0g\12T\274\32%J\266D\1h\12d\304\212"
|
|
",[\42S\0i\10c\305\232P\252\14j\12s\275\252\64\212\224\12\0k\12d\304\212\254\64$\221"
|
|
"\24l\10c\305\12\251\313\0m\12E\304\12\245EI\224\2n\10D\304\212%\62\5o\11D\304\32"
|
|
"%\222\22\5p\12T\274\212%\32\222,\3q\11T\274\232!J\266\2r\11D\304\212$\261e\0"
|
|
"s\10C\305\232![\0t\13d\304\232,\232\262$J\0u\10D\304\212\310\224\14v\10C\305\212"
|
|
"\304R\1w\12E\304\212LI\224.\0x\11D\304\212(\221\224(y\13T\274\212HJ\206(Q"
|
|
"\0z\11D\304\212!*\15\1{\12t\304*%L\304(\24|\6a\306\212\3}\13t\304\12\61"
|
|
"\12\225\60\221\0~\10$\344\232DI\0\5\0\304\12\200\13u\274\212K\242T\266\260\4\201\14f"
|
|
"D\233!\11#-\312!\11\202\15hD<\65\12\243,\214\302$\16\203\15w<\214C\22F\71\220"
|
|
"\26\207A\204\13u\274\212\244\232t\313\241\10\205\17\206<\213\60\31\22\311\66D\245!\11\3\206\20\210"
|
|
"<\254\342\20]\302(L\246C\30E\0\207\15wD\334X\25\267\341\20\15\21\0\210\16w<\214\203"
|
|
"RQ\25I\212\324a\20\211\15f\304\213)\213\244,\222\222\245\0\0\0\0";
|
|
|
|
|
|
const uint8_t u8g2_font_ncenB08_symbols8_ahoy[173] U8G2_FONT_SECTION("u8g2_font_ncenB08_symbols8_ahoy") =
|
|
"\13\0\3\2\4\4\1\2\5\10\11\0\0\10\0\10\0\0\0\0\0\0\224A\14\207\305\70H\321\222H"
|
|
"k\334\6B\20\230\305\32\262\60\211\244\266\60T\243\34\326\0C\20\210\305S\243\60\312\302(\214\302("
|
|
"L\342\0D\16\210\315\70(i\224#\71\20W\207\3E\15\207\305xI\206\323\232nIS\1F\25"
|
|
"\230\305H\206\244\230$C\22\15Y\242\204j\224\205I$\5G\17\210\305*\16\321%\214\302d:\204"
|
|
"Q\4H\14w\307\215Uq\33\16\321\20\1I\21\227\305\311\222aP\245H\221\244H\212\324a\20J"
|
|
"\5\0\275\0K\5\0\315\0\0\0\0";
|
|
|
|
const uint8_t u8g2_font_ncenB10_symbols10_ahoy[217] U8G2_FONT_SECTION("u8g2_font_ncenB10_symbols10_ahoy") =
|
|
"\13\0\3\2\4\4\2\2\5\13\13\0\0\13\0\13\0\0\0\0\0\0\300A\15\267\212q\220\42\251\322"
|
|
"\266\306\275\1B\20\230\236\65da\22Ima\250F\71\254\1C\23\272\272\251\3Q\32\366Q\212\243"
|
|
"\70\212\243\70\311\221\0D\20\271\252\361\242F:\242#: {\36\16\1E\17\267\212\221\264\3Q\35"
|
|
"\332\321\34\316\341\14F\25\250\233\221\14I\61I\206$\252%J\250Fa\224%J\71G\30\273\312W"
|
|
"\316r`T\262DJ\303\64L#%K\304\35\310\342,\3H\27\272\272\217\344P\16\351\210\16\354\300"
|
|
"<\244C\70,\303 \16!\0I\24\271\252\241\34\336\1-\223\64-\323\62-\323\62\35x\10J\22"
|
|
"\210\232\61Hi\64Di\64DI\226$KeiK\5\0\212\1\0\0\0";
|
|
|