diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 2e3dcb31..7eaf9b34 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -147,8 +147,6 @@ jobs: with: merge-multiple: true path: firmware - - name: Display structure of downloaded files - run: ls -R firmware - name: Get Version from code id: version_name @@ -161,19 +159,6 @@ jobs: env: VERSION: ${{ steps.version_name.outputs.name }} -# - name: Create Manifest -# working-directory: src -# run: python ../scripts/buildManifest.py -# -# - name: Create Artifact -# uses: actions/upload-artifact@v3 -# with: -# name: ahoydtu_dev -# path: | -# src/firmware/* -# src/User_Manual.md -# src/install.html - - name: Rename firmware directory run: mv firmware ${{ steps.version_name.outputs.name }} diff --git a/src/CHANGES.md b/src/CHANGES.md index cddfc86a..5073a2c9 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,9 @@ # Development Changes +## 0.8.48 - 2024-01-07 +* merge PR: pin selection for ESP-32 S2 #1334 +* merge PR: enhancement: power graph display option #1330 + ## 0.8.47 - 2024-01-06 * reduce GxEPD2 lib to compile faster * upgraded GxEPD2 lib to `1.5.3` diff --git a/src/defines.h b/src/defines.h index 87b4101d..bd5cc7ff 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 47 +#define VERSION_PATCH 48 //------------------------------------- typedef struct { diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index c5aa2ed3..c7235815 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -22,256 +22,255 @@ #include "../../utils/timemonitor.h" class DisplayMono { - public: - DisplayMono() {}; - - virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; - virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) = 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(uint8_t lum, bool motion) { - - bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) && - ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor - - if (mDisplayActive) { - if (!dispConditions) { - if (mDisplayTime.isTimeout()) { // switch display off after timeout - mDisplayActive = false; - mDisplay->setPowerSave(true); - DBGPRINTLN("**** Display off ****"); + public: + DisplayMono() {}; + + virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0; + virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum, uint8_t graph_ratio, uint8_t graph_size) = 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(uint8_t lum, bool motion) { + + bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) && + ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor + + if (mDisplayActive) { + if (!dispConditions) { + if (mDisplayTime.isTimeout()) { // switch display off after timeout + mDisplayActive = false; + mDisplay->setPowerSave(true); + DBGPRINTLN("**** Display off ****"); + } } + else + mDisplayTime.reStartTimeMonitor(); // keep display on } - else - mDisplayTime.reStartTimeMonitor(); // keep display on - } - else { - if (dispConditions) { - mDisplayActive = true; - mDisplayTime.reStartTimeMonitor(); // switch display on - mDisplay->setPowerSave(false); - DBGPRINTLN("**** Display on ****"); + else { + if (dispConditions) { + mDisplayActive = true; + mDisplayTime.reStartTimeMonitor(); // switch display on + mDisplay->setPowerSave(false); + DBGPRINTLN("**** Display on ****"); + } + } + + if(mLuminance != lum) { + mLuminance = lum; + mDisplay->setContrast(mLuminance); } - } - if(mLuminance != lum) { - mLuminance = lum; + return(monoMaintainDispSwitchState()); + } + + protected: + enum class DispSwitchState { + TEXT, + GRAPH + }; + + protected: + U8G2* mDisplay; + DisplayData *mDisplayData; + + float *mPgData = nullptr; + uint8_t mPgWidth = 0; + uint8_t mPgHeight = 0; + float mPgMaxPwr = 0.0; + uint32_t mPgPeriod = 0; // seconds + uint32_t mPgTimeOfDay = 0; + uint8_t mPgLastPos = 0; + + uint8_t mType; + uint16_t mDispWidth; + uint16_t mDispHeight; + + bool mEnPowerSave; + uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor + uint8_t mLuminance; + uint8_t mGraphRatio; + uint8_t mGraphSize; + + uint8_t mLoopCnt; + uint8_t mLineXOffsets[5] = {}; + uint8_t mLineYOffsets[5] = {}; + + uint8_t mExtra; + int8_t mPixelshift=0; + TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); + TimeMonitor mDispSwitchTime = TimeMonitor(); + DispSwitchState mDispSwitchState = DispSwitchState::TEXT; + bool mDisplayActive = true; // always start with display on + char mFmtText[DISP_FMT_TEXT_LEN]; + + // Common initialization function to be called by subclasses + void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) { + mDisplay = display; + mType = type; + mDisplayData = displayData; + mDisplay->begin(); + mDisplay->setPowerSave(false); // always start with display on mDisplay->setContrast(mLuminance); - } - - return(monoMaintainDispSwitchState()); - } - - protected: - U8G2* mDisplay; - DisplayData *mDisplayData; - - float *mPgData=nullptr; - uint8_t mPgWidth=0; - uint8_t mPgHeight=0; - float mPgMaxPwr=0.0; -// float mPgMaxAvailPower = 0.0; - uint32_t mPgPeriod=0; // seconds - uint32_t mPgTimeOfDay=0; - uint8_t mPgLastPos=0; - - uint8_t mType; - uint16_t mDispWidth; - uint16_t mDispHeight; - - bool mEnPowerSave; - uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor - uint8_t mLuminance; - uint8_t mGraphRatio; - uint8_t mGraphSize; - - uint8_t mLoopCnt; - uint8_t mLineXOffsets[5] = {}; - uint8_t mLineYOffsets[5] = {}; - - uint8_t mExtra; - int8_t mPixelshift=0; - TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); - TimeMonitor mDispSwitchTime = TimeMonitor(); - uint8_t mDispSwitchState; - bool mDisplayActive = true; // always start with display on - char mFmtText[DISP_FMT_TEXT_LEN]; - - enum _dispSwitchState { - d_POWER_TEXT = 0, - d_POWER_GRAPH = 1, - }; - - // Common initialization function to be called by subclasses - void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) { - mDisplay = display; - mType = type; - 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(); - mDispSwitchState = d_POWER_TEXT; - if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode - mDispSwitchState = d_POWER_GRAPH; - else if (mGraphRatio != 0) - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start display mode change only if ratio is neither 0 nor 100 - } - - bool monoMaintainDispSwitchState(void) { - bool change = false; - switch(mDispSwitchState) { - case d_POWER_TEXT: - if (mDispSwitchTime.isTimeout()) { - mDispSwitchState = d_POWER_GRAPH; - mDispSwitchTime.startTimeMonitor(150 * mGraphRatio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms - change = true; - } - break; - case d_POWER_GRAPH: - if (mDispSwitchTime.isTimeout()) { - mDispSwitchState = d_POWER_TEXT; - mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); - change = true; - } - break; - } - return change; - } - - void initPowerGraph(uint8_t width, uint8_t height) { - mPgWidth = width; - mPgHeight = height; - mPgData = new float[mPgWidth]; - //memset(mPgData, 0, mPgWidth); - resetPowerGraph(); + mDisplay->clearBuffer(); + mDispWidth = mDisplay->getDisplayWidth(); + mDispHeight = mDisplay->getDisplayHeight(); + mDispSwitchTime.stopTimeMonitor(); + if (mGraphRatio == 100) // if graph ratio is 100% start in graph mode + mDispSwitchState = DispSwitchState::GRAPH; + else if (mGraphRatio != 0) + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); // start display mode change only if ratio is neither 0 nor 100 + } + + bool monoMaintainDispSwitchState(void) { + bool change = false; + switch(mDispSwitchState) { + case DispSwitchState::TEXT: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = DispSwitchState::GRAPH; + mDispSwitchTime.startTimeMonitor(150 * mGraphRatio); // mGraphRatio: 0-100 Gesamtperiode 15000 ms + change = true; + } + break; + case DispSwitchState::GRAPH: + if (mDispSwitchTime.isTimeout()) { + mDispSwitchState = DispSwitchState::TEXT; + mDispSwitchTime.startTimeMonitor(150 * (100 - mGraphRatio)); + change = true; + } + break; + } + return change; + } + + void initPowerGraph(uint8_t width, uint8_t height) { + mPgWidth = width; + mPgHeight = height; + mPgData = new float[mPgWidth]; + //memset(mPgData, 0, mPgWidth); + resetPowerGraph(); /* - Inverter<> *iv; - mPgMaxAvailPower = 0; - uint8_t nInv = mSys->getNumInverters(); - for (uint8_t i = 0; i < nInv; i++) { - iv = mSys->getInverterByPos(i); - if (iv == NULL) - continue; - for (uint8_t ch = 0; ch < 6; ch++) { - mPgMaxAvailPower += iv->config->chMaxPwr[ch]; + Inverter<> *iv; + mPgMaxAvailPower = 0; + uint8_t nInv = mSys->getNumInverters(); + for (uint8_t i = 0; i < nInv; i++) { + iv = mSys->getInverterByPos(i); + if (iv == NULL) + continue; + for (uint8_t ch = 0; ch < 6; ch++) { + mPgMaxAvailPower += iv->config->chMaxPwr[ch]; + } } - } - DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ - } - - void resetPowerGraph() { - if (mPgData != nullptr) { - mPgMaxPwr = 0.0; - mPgLastPos = 0; - for (uint8_t i = 0; i < mPgWidth; i++) - mPgData[i] = 0.0; + DBGPRINTLN("max. Power = " + String(mPgMaxAvailPower));*/ } - } - uint8_t sss2pgpos(uint seconds_since_start) { - return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); - } + void resetPowerGraph() { + if (mPgData != nullptr) { + mPgMaxPwr = 0.0; + mPgLastPos = 0; + for (uint8_t i = 0; i < mPgWidth; i++) + mPgData[i] = 0.0; + } + } - void calcPowerGraphValues() { - mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis - uint32_t oldTimeOfDay = mPgTimeOfDay; - mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time - if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data - resetPowerGraph(); - mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day - } - - void addPowerGraphEntry(float val) { - if (mDisplayData->utcTs > 0) { // precondition: utc time available - calcPowerGraphValues(); - //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 - mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); - mPgMaxPwr = std::max(mPgMaxPwr, val); // max value of stored data for scaling of y-axis - } - } - - uint8_t getPowerGraphXpos(uint8_t p) { // - if ((p <= mPgLastPos) && (mPgLastPos > 0)) - return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis - else - return(0); - } - - uint8_t getPowerGraphYpos(uint8_t p) { - if (p < mPgWidth) - //return(((uint32_t) mPgData[p] * (uint32_t) mPgMaxAvailPower) * (uint32_t) mPgHeight / mPgMaxPwr / 255); // scaling of normalized data (0-255) to graph height - return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height - else - return(0); - } - - void plotPowerGraph(uint8_t xoff, uint8_t yoff) { - // draw axes - mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis - mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis - - // draw X scale - tmElements_t tm; - breakTime(mDisplayData->pGraphEndTime, tm); - uint8_t endHourPg = tm.Hour; - breakTime(mDisplayData->utcTs, tm); - uint8_t endHour = std::min(endHourPg, tm.Hour); - breakTime(mDisplayData->pGraphStartTime, 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) - mDisplayData->pGraphStartTime)); // scale horizontal axis - mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); + uint8_t sss2pgpos(uint seconds_since_start) { + return(seconds_since_start * (mPgWidth - 1) / (mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime)); } - // draw Y scale - uint16_t scale_y = 10; - uint32_t maxpwr_int = static_cast(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(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis - mDisplay->drawPixel(xoff + 1, ypos); + void calcPowerGraphValues() { + mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // length of power graph for scaling of x-axis + uint32_t oldTimeOfDay = mPgTimeOfDay; + mPgTimeOfDay = (mDisplayData->utcTs > mDisplayData->pGraphStartTime) ? mDisplayData->utcTs - mDisplayData->pGraphStartTime : 0; // current time of day with respect to current sunrise time + if (oldTimeOfDay > mPgTimeOfDay) // new day -> reset old data + resetPowerGraph(); + mPgLastPos = std::min((uint8_t) (mPgTimeOfDay * (mPgWidth - 1) / mPgPeriod), (uint8_t) (mPgWidth - 1)); // current datapoint based on currenct time of day + } + + void addPowerGraphEntry(float val) { + if (mDisplayData->utcTs > 0) { // precondition: utc time available + calcPowerGraphValues(); + //mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], (uint8_t) (val * 255.0 / mPgMaxAvailPower)); // normalizing of data to 0-255 + mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); + mPgMaxPwr = std::max(mPgMaxPwr, val); // max value of stored data for scaling of y-axis + } } - // draw curve - for (uint8_t i = 1; i <= mPgLastPos; i++) { - mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphYpos(i - 1), - xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + uint8_t getPowerGraphXpos(uint8_t p) { // + if ((p <= mPgLastPos) && (mPgLastPos > 0)) + return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis + else + return(0); } - // print max power value - mDisplay->setFont(u8g2_font_4x6_tr); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); - mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); - } - - // pixelshift screensaver with wipe effect - void calcPixelShift(int range) { - int8_t mod = (millis() / 10000) % ((range >> 1) << 2); - mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; - } + uint8_t getPowerGraphYpos(uint8_t p) { + if (p < mPgWidth) + //return(((uint32_t) mPgData[p] * (uint32_t) mPgMaxAvailPower) * (uint32_t) mPgHeight / mPgMaxPwr / 255); // scaling of normalized data (0-255) to graph height + return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height + else + return(0); + } + + void plotPowerGraph(uint8_t xoff, uint8_t yoff) { + // draw axes + mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis + mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis + + // draw X scale + tmElements_t tm; + breakTime(mDisplayData->pGraphEndTime, tm); + uint8_t endHourPg = tm.Hour; + breakTime(mDisplayData->utcTs, tm); + uint8_t endHour = std::min(endHourPg, tm.Hour); + breakTime(mDisplayData->pGraphStartTime, 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) - mDisplayData->pGraphStartTime)); // scale horizontal axis + mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1); + } + + // draw Y scale + uint16_t scale_y = 10; + uint32_t maxpwr_int = static_cast(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(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 - getPowerGraphYpos(i - 1), + xoff + getPowerGraphXpos(i), yoff - getPowerGraphYpos(i)); + } + + // print max power value + mDisplay->setFont(u8g2_font_4x6_tr); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr))); + mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText); + } + + // pixelshift screensaver with wipe effect + void calcPixelShift(int range) { + int8_t mod = (millis() / 10000) % ((range >> 1) << 2); + mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0; + } }; /* 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 */ + \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" diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 32459c89..c8338691 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -176,7 +176,7 @@ class DisplayMono128X64 : public DisplayMono { printText(mFmtText, l_YieldTotal, 0xff); } - if (mDispSwitchState == d_POWER_GRAPH) { + if (mDispSwitchState == DispSwitchState::GRAPH) { // plot power graph plotPowerGraph((mDispWidth - mPgWidth) / 2 + mPixelshift, mLineYOffsets[graph_last_line] - 1); } @@ -274,6 +274,6 @@ class DisplayMono128X64 : public DisplayMono { } bool showLine(uint8_t line) { - return ((mDispSwitchState == d_POWER_TEXT) || ((line < graph_first_line) || (line > graph_last_line))); + return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line))); } }; diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index ee2aebd5..b195e17c 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -137,7 +137,7 @@ class DisplayMono84X48 : public DisplayMono { printText(mFmtText, l_YieldTotal, 0xff); } - if (mDispSwitchState == d_POWER_GRAPH) { + if (mDispSwitchState == DispSwitchState::GRAPH) { // plot power graph plotPowerGraph(8, mLineYOffsets[graph_last_line] - 1); } @@ -227,7 +227,7 @@ class DisplayMono84X48 : public DisplayMono { } bool showLine(uint8_t line) { - return ((mDispSwitchState == d_POWER_TEXT) || ((line < graph_first_line) || (line > graph_last_line))); + return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line))); } };