From 2972660cdda5ebafb03754dd9b84bbc5afed39e3 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Mon, 24 Jul 2023 15:19:03 +0200 Subject: [PATCH 1/9] [P036] Add ticker as scroll option --- docs/source/Plugin/P036_ScrollOptions.png | Bin 0 -> 29566 bytes docs/source/Plugin/P036_commands.repl | 18 +- lib/esp8266-oled-ssd1306/OLEDDisplay.cpp | 9 + lib/esp8266-oled-ssd1306/OLEDDisplay.h | 5 + src/Custom-sample.h | 1 + src/_P036_FrameOLED.ino | 216 ++++++---- src/src/PluginStructs/P036_data_struct.cpp | 476 +++++++++++++++------ src/src/PluginStructs/P036_data_struct.h | 123 ++++-- 8 files changed, 586 insertions(+), 262 deletions(-) create mode 100644 docs/source/Plugin/P036_ScrollOptions.png diff --git a/docs/source/Plugin/P036_ScrollOptions.png b/docs/source/Plugin/P036_ScrollOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..c549d93db47de2f25eca157b1fce425f9b1247d7 GIT binary patch literal 29566 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 Vl~f parameter corresponds to the desired frame (1..) to display. The number of frames is determined by dividing the lines in use (at least one line in that frame with some data), by the number of Lines per Frame. So practically, the range is 1..3 when all lines are used and 4 Lines per Frame is set, or 1..12 if Line per frames is set to 1. The number of frames is updated if a frame would initially be empty, and an external source places text on a line of that frame (see above). + If scroll is set to ``ticker`` only = 1 is supported, it starts the ticker from the beginning. When omitting , or providing 0, the next frame is displayed. @@ -59,6 +61,7 @@ ``oledframedcmd,linecount,<1..4>`` "," This command changes the number of lines in each frame. When the next frame is to be displayed, the frames are recalculated and the sequence is restarted at the first frame. + If scroll is set to ``ticker`` this command is not supported. If Generate events for 'Linecount' is selected, a ```` event is generated on initialization of the plugin and when changing the setting. " @@ -71,4 +74,17 @@ ``oledframedcmd,align,<0|1|2>`` "," Set the global align option for content to centre (0), left (1) or right (2). - " \ No newline at end of file + " + " + ``oledframedcmd,restore,`` + "," + If the parameter is set to 0 all line contents will be restored from the settings. + Otherwise the parameter corresponds with the same lines as the plugin configuration has, + and only the content of this line will be restored from the settings. + " + " + ``oledframedcmd,scroll,`` + "," + The parameter corresponds with the line number of the scroll parameter of the settings (1=Very slow ... 6=Ticker). + After applying the new scroll speed the display restarts with the first page. + " diff --git a/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp b/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp index 40986a3d19..8978c48fe7 100644 --- a/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp +++ b/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp @@ -532,6 +532,15 @@ uint16_t OLEDDisplay::getStringWidth(const String& strUser) { return width; } +uint8_t OLEDDisplay::getCharWidth(const char c) { + uint8_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + if (utf8ascii(c) == 0) + return 0; + if (c < firstChar) + return 0; + return pgm_read_byte(fontData + JUMPTABLE_START + (c- firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); +} + void OLEDDisplay::setTextAlignment(OLEDDISPLAY_TEXT_ALIGNMENT textAlignment) { this->textAlignment = textAlignment; } diff --git a/lib/esp8266-oled-ssd1306/OLEDDisplay.h b/lib/esp8266-oled-ssd1306/OLEDDisplay.h index 978fc9cc10..16433c0a64 100644 --- a/lib/esp8266-oled-ssd1306/OLEDDisplay.h +++ b/lib/esp8266-oled-ssd1306/OLEDDisplay.h @@ -190,6 +190,11 @@ class OLEDDisplay : public Print { // Convencience method for the const char version uint16_t getStringWidth(const String& text); + // Returns the width of c with the already set fontData + // returns a 0 if c is non-ascii + // in this case the next char must be converted + uint8_t getCharWidth(const char c); + // Specifies relative to which anchor point // the text is rendered. Available constants: // TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER_BOTH diff --git a/src/Custom-sample.h b/src/Custom-sample.h index ccd4794b1a..3f43681837 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -376,6 +376,7 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = { // #define USES_P033 // Dummy // #define USES_P034 // DHT12 // #define USES_P036 // FrameOLED +// #define P036_ENABLE_TICKER 1 // Enable ticker function // #define USES_P037 // MQTTImport // #define P037_MAPPING_SUPPORT 1 // Enable Value mapping support // #define P037_FILTER_SUPPORT 1 // Enable filtering support diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index f6bda274c1..77e82ba740 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -14,6 +14,11 @@ // Added to the main repository with some optimizations and some limitations. // Al long as the device is not selected, no RAM is waisted. // +// @uwekaditz: 2023-07-23 +// NEW: Add ticker for scrolling speed, solves issue #4188 +// ADD: Setting and support for oledframedcmd,restore,<0|> subcommand par2: (0=all|Line Content) +// ADD: Setting and support for oledframedcmd,scroll,<1..6> subcommand, par2: (casted to ePageScrollSpeeds) +// CHG: Minor change in debug messages (addLogMove() for dynamic messages) // @tonhuisman: 2023-04-30 // FIX: Loading and saving line-settings for font and alignment used overlapping page-variables // @tonhuisman: 2023-03-07 @@ -192,6 +197,8 @@ # define P036_EVENT_FRAME 2 // event: #frame=1..n # define P036_EVENT_LINE 3 // event: #line=1..n # define P036_EVENT_LINECNT 4 // event: #linecount=1..4 +# define P036_EVENT_RESTORE 5 // event: #restore=1..n +# define P036_EVENT_SCROLL 6 // event: #scroll=ePSS_VerySlow..ePSS_Ticker void P036_SendEvent(struct EventStruct *event, uint8_t eventId, int16_t eventValue); @@ -299,20 +306,32 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } # endif // ifdef P036_ENABLE_LEFT_ALIGN { - const __FlashStringHelper *options[5] = { +# if P036_ENABLE_TICKER + const int optionCnt = 6; +# else // if P036_ENABLE_TICKER + const int optionCnt = 5; +# endif // if P036_ENABLE_TICKER + const __FlashStringHelper *options[optionCnt] = { F("Very Slow"), F("Slow"), F("Fast"), F("Very Fast"), - F("Instant") + F("Instant"), +# if P036_ENABLE_TICKER + F("Ticker"), +# endif // if P036_ENABLE_TICKER }; - const int optionValues[5] = + const int optionValues[optionCnt] = { static_cast(ePageScrollSpeed::ePSS_VerySlow), static_cast(ePageScrollSpeed::ePSS_Slow), static_cast(ePageScrollSpeed::ePSS_Fast), static_cast(ePageScrollSpeed::ePSS_VeryFast), - static_cast(ePageScrollSpeed::ePSS_Instant) }; - addFormSelector(F("Scroll"), F("scroll"), 5, options, optionValues, P036_SCROLL); + static_cast(ePageScrollSpeed::ePSS_Instant), +# if P036_ENABLE_TICKER + static_cast(ePageScrollSpeed::ePSS_Ticker), +# endif // if P036_ENABLE_TICKER + }; + addFormSelector(F("Scroll"), F("scroll"), optionCnt, options, optionValues, P036_SCROLL); } // FIXME TD-er: Why is this using pin3 and not pin1? And why isn't this using the normal pin selection functions? @@ -667,6 +686,7 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) (P036_ROTATE == 2), // 1 = Normal, 2 = Rotated P036_CONTRAST, P036_TIMER, + static_cast(P036_SCROLL), // Scroll speed P036_NLINES ))) { clearPluginTaskData(event->TaskIndex); @@ -969,9 +989,9 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) // Define Scroll area layout P036_data->P036_DisplayPage(event); } else { - # ifdef PLUGIN_036_DEBUG + # ifdef PLUGIN_036_DEBUG addLog(LOG_LEVEL_INFO, F("P036_PLUGIN_READ Page scrolling running")); - # endif // PLUGIN_036_DEBUG + # endif // PLUGIN_036_DEBUG } success = true; @@ -998,9 +1018,12 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) addLog(LOG_LEVEL_INFO, F("P036_PLUGIN_WRITE ...")); # endif // PLUGIN_036_DEBUG - String command = parseString(string, 1); - String subcommand = parseString(string, 2); - int LineNo = event->Par1; + bool bUpdateDisplay = false; + bool bDisplayON = false; + uint8_t eventId = 0; + String command = parseString(string, 1); + String subcommand = parseString(string, 2); + int LineNo = event->Par1; # ifdef P036_SEND_EVENTS bool sendEvents = bitRead(P036_FLAGS_0, P036_FLAG_SEND_EVENTS); // Bit 28 Send Events # endif // ifdef P036_SEND_EVENTS @@ -1041,49 +1064,25 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) if (equals(para1, F("low"))) { success = true; P036_data->setContrast(OLED_CONTRAST_LOW); - # ifdef P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 0); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // ifdef P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 0; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } if (equals(para1, F("med"))) { success = true; P036_data->setContrast(OLED_CONTRAST_MED); - # ifdef P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 1); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // ifdef P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 1; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } if (equals(para1, F("high"))) { success = true; P036_data->setContrast(OLED_CONTRAST_HIGH); - # ifdef P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 2); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // ifdef P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 2; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } if (equals(para1, F("user")) && @@ -1094,17 +1093,9 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) success = true; P036_data->display->setContrast(static_cast(event->Par3), static_cast(event->Par4), static_cast(event->Par5)); - # ifdef P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 3); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // ifdef P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 3; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } } else if ((equals(subcommand, F("frame"))) && (event->Par2 >= 0) && @@ -1136,11 +1127,19 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) else if ((equals(subcommand, F("linecount"))) && (event->Par2 >= 1) && (event->Par2 <= 4)) { + # if P036_ENABLE_TICKER + + if (static_cast(P036_SCROLL) == ePageScrollSpeed::ePSS_Ticker) { + // Ticker supports only 1 line, can not be changed + success = (event->Par2 == 1); + return success; + } + # endif // if P036_ENABLE_TICKER success = true; if (P036_NLINES != event->Par2) { P036_NLINES = event->Par2; - P036_data->setNrLines(P036_NLINES); + P036_data->setNrLines(event, P036_NLINES); # ifdef P036_SEND_EVENTS if (sendEvents && bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE)) { // Bit 29 Send Events Frame & Line @@ -1150,6 +1149,48 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } } # endif // P036_ENABLE_LINECOUNT + else if ((equals(subcommand, F("restore"))) && + (event->Par2 >= 0) && // 0: restore all line contents + (event->Par2 <= P36_Nlines)) { + // restore content functions + success = true; + LineNo = event->Par2; + P036_data->RestoreLineContent(event->TaskIndex, + get4BitFromUL(P036_FLAGS_0, P036_FLAG_SETTINGS_VERSION), // Bit23-20 Version CustomTaskSettings + LineNo); + + if (LineNo == 0) { + LineNo = 1; // after restoring all contents start with first Line + } + eventId = P036_EVENT_RESTORE; + bUpdateDisplay = true; + } + else if ((equals(subcommand, F("scroll"))) && + (event->Par2 >= 1)) { + // set scroll + success = true; + + switch (event->Par2) { + case 1: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_VerySlow); break; + case 2: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Slow); break; + case 3: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Fast); break; + case 4: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_VeryFast); break; + case 5: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Instant); break; +# if P036_ENABLE_TICKER + case 6: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Ticker); break; +# endif // if P036_ENABLE_TICKER + default: + success = false; + break; + } + + if (success) { + P036_data->prepare_pagescrolling(static_cast(P036_SCROLL), P036_NLINES); + eventId = P036_EVENT_SCROLL; + LineNo = 1; // after change scroll start with first Line + bUpdateDisplay = true; + } + } # ifdef P036_ENABLE_LEFT_ALIGN else if ((equals(subcommand, F("leftalign"))) && ((event->Par2 == 0) || @@ -1206,6 +1247,27 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) *currentLine = currentLine->substring(0, strlen - iCharToRemove); } } + eventId = P036_FLAG_EVENTS_FRAME_LINE; + bUpdateDisplay = true; + } + } + + if (success && (eventId > 0)) { + if (bDisplayON) { + # ifdef P036_SEND_EVENTS + + if (sendEvents) { + P036_SendEvent(event, eventId, LineNo); + + if (!P036_DisplayIsOn) { + P036_SendEvent(event, P036_EVENT_DISPLAY, 1); + } + } + # endif // ifdef P036_SEND_EVENTS + P036_SetDisplayOn(1); // Save the fact that the display is now ON + } + + if (bUpdateDisplay) { P036_data->MaxFramesToDisplay = 0xff; // update frame count # ifdef P036_SEND_EVENTS @@ -1213,10 +1275,11 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) # endif // ifdef P036_SEND_EVENTS if (!P036_DisplayIsOn && - !bitRead(P036_FLAGS_0, P036_FLAG_NODISPLAY_ONRECEIVE)) { // Bit 18 NoDisplayOnReceivedText + (!bitRead(P036_FLAGS_0, P036_FLAG_NODISPLAY_ONRECEIVE) || // Bit 18 NoDisplayOnReceivedText + (eventId == P036_EVENT_SCROLL))) { // display was OFF, turn it ON P036_data->display->displayOn(); - P036_SetDisplayOn(1); // Save the fact that the display is now ON + P036_SetDisplayOn(1); // Save the fact that the display is now ON # ifdef P036_SEND_EVENTS if (sendEvents) { @@ -1241,27 +1304,30 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } # ifdef PLUGIN_036_DEBUG - String log; - - if (loglevelActiveFor(LOG_LEVEL_INFO) && - log.reserve(200)) { // estimated - log += F("[P36] Line: "); - log += LineNo; - log += F(" NewContent:"); - log += NewContent; - log += F(" Content:"); - log += P036_data->DisplayLinesV1[LineNo - 1].Content; - log += F(" Length:"); - log += P036_data->DisplayLinesV1[LineNo - 1].Content.length(); - log += F(" Pix: "); - log += P036_data->display->getStringWidth(P036_data->DisplayLinesV1[LineNo - 1].Content); - log += F(" Reserved:"); - log += P036_data->DisplayLinesV1[LineNo - 1].reserved; - addLogMove(LOG_LEVEL_INFO, log); + + if (eventId == P036_FLAG_EVENTS_FRAME_LINE) { + String log; + + if (loglevelActiveFor(LOG_LEVEL_INFO) && + log.reserve(200)) { // estimated + log = F("[P36] Line: "); + log += LineNo; + log += F(" Content:"); + log += P036_data->LineContent->DisplayLinesV1[LineNo - 1].Content; + log += F(" Length:"); + log += P036_data->LineContent->DisplayLinesV1[LineNo - 1].Content.length(); + log += F(" Pix: "); + log += P036_data->display->getStringWidth(P036_data->LineContent->DisplayLinesV1[LineNo - 1].Content); + log += F(" Reserved:"); + log += P036_data->LineContent->DisplayLinesV1[LineNo - 1].reserved; + addLogMove(LOG_LEVEL_INFO, log); + delay(5); // otherwise it is may be to fast for the serial monitor + } } # endif // PLUGIN_036_DEBUG } } + # ifdef PLUGIN_036_DEBUG if (!success && loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -1291,6 +1357,8 @@ const __FlashStringHelper* P36_eventId_toString(uint8_t eventId) # ifdef P036_ENABLE_LINECOUNT case P036_EVENT_LINECNT: return F("linecount"); # endif // P036_ENABLE_LINECOUNT + case P036_EVENT_RESTORE: return F("restore"); + case P036_EVENT_SCROLL: return F("scroll"); } return F(""); } diff --git a/src/src/PluginStructs/P036_data_struct.cpp b/src/src/PluginStructs/P036_data_struct.cpp index 418b76ac10..ba5d00ab36 100644 --- a/src/src/PluginStructs/P036_data_struct.cpp +++ b/src/src/PluginStructs/P036_data_struct.cpp @@ -114,31 +114,18 @@ void P036_data_struct::reset() { # ifdef P036_FONT_CALC_LOG const __FlashStringHelper * tFontSettings::FontName() const { - if (fontData == ArialMT_Plain_24) { - return F("Arial_24"); - } - -# ifndef P036_LIMIT_BUILD_SIZE - if (fontData == Dialog_plain_18) { - return F("Dialog_18"); - } -# endif // ifndef P036_LIMIT_BUILD_SIZE - - if (fontData == ArialMT_Plain_16) { - return F("Arial_16"); - } - -# ifndef P036_LIMIT_BUILD_SIZE - if (fontData == Dialog_plain_12) { - return F("Dialog_12"); - } -# endif // ifndef P036_LIMIT_BUILD_SIZE - - if (fontData == ArialMT_Plain_10) { - return F("Arial_10"); - } - else { - return F("Unknown font"); + switch (fontIdx) { + case 0: return F("Arial_24"); break; + # ifndef P036_LIMIT_BUILD_SIZE + case 1: return F("Dialog_18"); break; + case 2: return F("Arial_16"); break; + case 3: return F("Dialog_12"); break; + case 4: return F("Arial_10"); break; + # else // ifndef P036_LIMIT_BUILD_SIZE + case 1: return F("Arial_16"); break; + case 2: return F("Arial_10"); break; + # endif // ifndef P036_LIMIT_BUILD_SIZE + default: return F("Unknown font"); } } @@ -148,15 +135,15 @@ const __FlashStringHelper * tFontSettings::FontName() const { // The same as when using the DRAM_ATTR attribute used for interrupt code. // This is very precious memory, so we must find something other way to define this. const tFontSizes FontSizes[P36_MaxFontCount] = { - { getArialMT_Plain_24(), 24, 28 }, // 9643 + { getArialMT_Plain_24(), 24, 28 }, // 9643 # ifndef P036_LIMIT_BUILD_SIZE - { getDialog_plain_18(), 19, 22 }, + { getDialog_plain_18(), 19, 22 }, // 7399 # endif // ifndef P036_LIMIT_BUILD_SIZE - { getArialMT_Plain_16(), 16, 19 }, // 5049 + { getArialMT_Plain_16(), 16, 19 }, // 5049 # ifndef P036_LIMIT_BUILD_SIZE - { getDialog_plain_12(), 13, 15 }, // 3707 + { getDialog_plain_12(), 13, 15 }, // 3707 # endif // ifndef P036_LIMIT_BUILD_SIZE - { getArialMT_Plain_10(), 10, 13 }, // 2731 + { getArialMT_Plain_10(), 10, 13 }, // 2731 }; const tSizeSettings SizeSettings[P36_MaxSizesCount] = { @@ -183,17 +170,18 @@ const tSizeSettings& P036_data_struct::getDisplaySizeSettings(p036_resolution di return SizeSettings[index]; } -bool P036_data_struct::init(taskIndex_t taskIndex, - uint8_t LoadVersion, - uint8_t Type, - uint8_t Address, - uint8_t Sda, - uint8_t Scl, - p036_resolution Disp_resolution, - bool Rotated, - uint8_t Contrast, - uint16_t DisplayTimer, - uint8_t NrLines) { +bool P036_data_struct::init(taskIndex_t taskIndex, + uint8_t LoadVersion, + uint8_t Type, + uint8_t Address, + uint8_t Sda, + uint8_t Scl, + p036_resolution Disp_resolution, + bool Rotated, + uint8_t Contrast, + uint16_t DisplayTimer, + ePageScrollSpeed ScrollSpeed, + uint8_t NrLines) { reset(); lastWiFiState = P36_WIFI_STATE_UNSET; @@ -256,12 +244,11 @@ bool P036_data_struct::init(taskIndex_t taskIndex, update_display(); // Initialize frame counter - frameCounter = 0; - currentFrameToDisplay = 0; - nextFrameToDisplay = 0; - bPageScrollDisabled = true; // first page after INIT without scrolling - ScrollingPages.linesPerFrameDef = NrLines; - bLineScrollEnabled = false; // start without line scrolling + frameCounter = 0; + currentFrameToDisplay = 0; + nextFrameToDisplay = 0; + bPageScrollDisabled = true; // first page after INIT without scrolling + bLineScrollEnabled = false; // start without line scrolling // Clear scrolling line data for (uint8_t i = 0; i < P36_MAX_LinesPerPage; i++) { @@ -270,7 +257,7 @@ bool P036_data_struct::init(taskIndex_t taskIndex, } // prepare font and positions for page and line scrolling - prepare_pagescrolling(); + prepare_pagescrolling(ScrollSpeed, NrLines); } return isInitialized(); @@ -293,20 +280,38 @@ void P036_data_struct::setOrientationRotated(bool rotated) { } } +void P036_data_struct::RestoreLineContent(taskIndex_t taskIndex, + uint8_t LoadVersion, + uint8_t LineNo) { + P036_LineContent *TempContent = new (std::nothrow) P036_LineContent(); + + if (TempContent != nullptr) { + TempContent->loadDisplayLines(taskIndex, LoadVersion); + + if (LineNo == 0) { + for (int i = 0; i < P36_Nlines; ++i) { + *(&LineContent->DisplayLinesV1[i].Content) = TempContent->DisplayLinesV1[i].Content; + } + } + else { + *(&LineContent->DisplayLinesV1[LineNo - 1].Content) = TempContent->DisplayLinesV1[LineNo - 1].Content; + } + delete TempContent; + } +} + # ifdef P036_ENABLE_LINECOUNT -void P036_data_struct::setNrLines(uint8_t NrLines) { +void P036_data_struct::setNrLines(struct EventStruct *event, uint8_t NrLines) { if ((NrLines >= 1) && (NrLines <= 4)) { - ScrollingPages.linesPerFrameDef = NrLines; - prepare_pagescrolling(); // Recalculate font - MaxFramesToDisplay = 0xFF; // Recalculate page indicator - CalcMaxPageCount(); // Update max page count - nextFrameToDisplay = 0; // Reset to first page + prepare_pagescrolling(static_cast(P036_SCROLL), NrLines); // Recalculate font + MaxFramesToDisplay = 0xFF; // Recalculate page indicator + CalcMaxPageCount(); // Update max page count + nextFrameToDisplay = 0; // Reset to first page } } # endif // P036_ENABLE_LINECOUNT - void P036_data_struct::display_header() { if (!isInitialized()) { return; @@ -534,7 +539,7 @@ int16_t P036_data_struct::GetHeaderHeight() { } int16_t P036_data_struct::GetIndicatorTop() { - if (bHideFooter) { + if (bHideFooter || bUseTicker) { // no footer (indicator) -> returm max. display height return getDisplaySizeSettings(disp_resolution).Height; } @@ -678,7 +683,7 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log; log.reserve(80); - log += F("P036 CalculateFontSettings lines: "); + log = F("P036 CalculateFontSettings lines: "); log += iLinesPerFrame; log += F(", height: "); log += iHeight; @@ -688,23 +693,23 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { log += boolToString(!bHideFooter); addLogMove(LOG_LEVEL_INFO, log); } - String log; # endif // ifdef P036_FONT_CALC_LOG iMaxHeightForFont = lround(iHeight / (iLinesPerFrame * 1.0f)); // no extra space between lines // Fonts already have their own extra space, no need to add an extra pixel space # ifdef P036_FONT_CALC_LOG + delay(5); // otherwise it is may be to fast for the serial monitor String log; log.reserve(80); log.clear(); - log += F("CalculateFontSettings LinesPerFrame: "); + log = F("CalculateFontSettings LinesPerFrame: "); log += iLinesPerFrame; log += F(", iHeight: "); log += iHeight; log += F(", maxFontHeight: "); log += iMaxHeightForFont; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef P036_FONT_CALC_LOG while (iFontIndex < 0) { @@ -717,7 +722,8 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { for (i = 0; i < P36_MaxFontCount - 1; i++) { // check available fonts for the line setting # ifdef P036_FONT_CALC_LOG - log1 += F(" -> i: "); + delay(5); // otherwise it is may be to fast for the serial monitor + log1 = F(" -> i: "); log1 += i; log1 += F(", h: "); log1 += FontSizes[i].Height; @@ -739,7 +745,7 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { # ifdef P036_FONT_CALC_LOG log1 += F(", no font fits, fontIdx: "); log1 += iFontIndex; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); # endif // ifdef P036_FONT_CALC_LOG break; @@ -792,6 +798,13 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { uint8_t iIdxForBiggestFont = 0; while (currentLine < P36_Nlines) { +# if P036_ENABLE_TICKER + + if (bUseTicker && (currentLine > 0)) { + // for ticker only the first line defines the font + break; + } +# endif // if P036_ENABLE_TICKER // calculate individual font settings IndividualFontSettings = CalculateIndividualFontSettings(currentLine, iFontIndex, @@ -822,6 +835,7 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { String log1; if (log1.reserve(140)) { // estimated + delay(5); // otherwise it is may be to fast for the serial monitor log1.clear(); log1 = F("IndividualFontSettings:"); log1 += F(" iFontIndex:"); log1 += iFontIndex; @@ -830,16 +844,17 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { log1 += F(" iHeight:"); log1 += iHeight; log1 += F(" iUsedHeightForFonts:"); log1 += iUsedHeightForFonts; log1 += F(" iMaxHeightForFont:"); log1 += iMaxHeightForFont; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); for (uint8_t i = 0; i < P36_Nlines; i++) { + delay(5); // otherwise it is may be to fast for the serial monitor log1.clear(); - log1 += F("Line["); log1 += i; + log1 = F("Line["); log1 += i; log1 += F("]: Frame:"); log1 += LineSettings[i].frame; log1 += F(" FontIdx:"); log1 += LineSettings[i].fontIdx; log1 += F(" ypos:"); log1 += LineSettings[i].ypos - TopLineOffset; log1 += F(" FontHeight:"); log1 += LineSettings[i].FontHeight; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); } } } @@ -854,6 +869,7 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { String log1; if (log1.reserve(140)) { // estimated + delay(5); // otherwise it is may be to fast for the serial monitor log1.clear(); log1 = F("CalculateFontSettings: Font:"); log1 += result.FontName(); @@ -886,15 +902,27 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { return result; } -void P036_data_struct::prepare_pagescrolling() { +void P036_data_struct::prepare_pagescrolling(ePageScrollSpeed lscrollspeed, + uint8_t NrLines) { if (!isInitialized()) { return; } +# if P036_ENABLE_TICKER + bUseTicker = (lscrollspeed == ePageScrollSpeed::ePSS_Ticker); +# else // if P036_ENABLE_TICKER + bUseTicker = false; +# endif //if P036_ENABLE_TICKER + + if (bUseTicker) { + ScrollingPages.linesPerFrameDef = 1; + } + else { + ScrollingPages.linesPerFrameDef = NrLines; + } CalculateFontSettings(0); } -uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTaskTimer) -{ +uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTaskTimer) { if (!isInitialized()) { return 0; } @@ -907,7 +935,7 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(32)) { - log += F("Start Scrolling: Speed: "); + log = F("Start Scrolling: Speed: "); log += static_cast(lscrollspeed); addLogMove(LOG_LEVEL_INFO, log); } @@ -919,6 +947,9 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (lscrollspeed == ePageScrollSpeed::ePSS_Instant) { // no scrolling, just the handling time to build the new page iPageScrollTime = P36_PageScrollTick - P36_PageScrollTimer; + } else if (lscrollspeed == ePageScrollSpeed::ePSS_Ticker) { + // for ticker, no scrolling, just the handling time to build the new page + iPageScrollTime = P36_PageScrollTick - P36_PageScrollTimer; } else { iPageScrollTime = (P36_MaxDisplayWidth / (P36_PageScrollPix * static_cast(lscrollspeed))) * P36_PageScrollTick; } @@ -929,7 +960,7 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log; log.reserve(32); - log += F("PageScrollTime: "); + log = F("PageScrollTime: "); log += iPageScrollTime; addLogMove(LOG_LEVEL_INFO, log); } @@ -942,6 +973,22 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas MaxPixWidthForPageScrolling -= getDisplaySizeSettings(disp_resolution).PixLeft; } +# if P036_ENABLE_TICKER + + if (bUseTicker) { + ScrollingLines.Ticker.Tcontent = EMPTY_STRING; + ScrollingLines.Ticker.IdxEnd = 0; + ScrollingLines.Ticker.IdxStart = 0; + + for (uint8_t i = 0; i < P36_Nlines; i++) { + String tmpString(LineContent->DisplayLinesV1[i].Content); + tmpString.replace(F("<|>"), " "); // replace the split token with three space char + ScrollingLines.Ticker.Tcontent += P36_parseTemplate(tmpString, i); + } + ScrollingLines.Ticker.len = ScrollingLines.Ticker.Tcontent.length(); + } +# endif // if P036_ENABLE_TICKER + for (uint8_t j = 0; j < ScrollingPages.linesPerFrameDef; j++) { // default no line scrolling and strings are centered uint16_t PixLengthLineOut = 0; // pix length of line out @@ -964,31 +1011,97 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas ScrollingLines.SLine[j].LastWidth = PixLengthLineOut; // while page scrolling this line is right aligned } - if ((PixLengthLineIn > getDisplaySizeSettings(disp_resolution).Width) && + if ((bUseTicker || (PixLengthLineIn > getDisplaySizeSettings(disp_resolution).Width)) && (iScrollTime > 0)) { // width of the line > display width -> scroll line - ScrollingLines.SLine[j].SLcontent = ScrollingPages.In[j].SPLcontent; - ScrollingLines.SLine[j].SLidx = ScrollingPages.In[j].SPLidx; // index to LineSettings[] - ScrollingLines.SLine[j].Width = PixLengthLineIn; // while page scrolling this line is left aligned - ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft; - ScrollingLines.SLine[j].fPixSum = getDisplaySizeSettings(disp_resolution).PixLeft; + if (bUseTicker) { +# if P036_ENABLE_TICKER + ScrollingLines.SLine[j].Width = 0; + uint16_t AddPixTicker; + + switch (textAlignment) { + case TEXT_ALIGN_CENTER: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft + + getDisplaySizeSettings(disp_resolution).Width / 2; + AddPixTicker = getDisplaySizeSettings(disp_resolution).Width / 2; // half width at begin + break; + case TEXT_ALIGN_RIGHT: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft + + getDisplaySizeSettings(disp_resolution).Width; + AddPixTicker = getDisplaySizeSettings(disp_resolution).Width; // full width at begin + break; + default: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft; + AddPixTicker = 0; + } + ScrollingLines.SLine[j].fPixSum = ScrollingLines.SLine[j].CurrentLeft; + + display->setFont(FontSizes[LineSettings[j].fontIdx].fontData); + ScrollingLines.SLine[j].dPix = (static_cast(display->getStringWidth(ScrollingLines.Ticker.Tcontent) + AddPixTicker)) / + static_cast(iScrollTime); + ScrollingLines.SLine[j].SLcontent = EMPTY_STRING; + + ScrollingLines.Ticker.TickerAvgPixPerChar = lround(static_cast(display->getStringWidth( + ScrollingLines.Ticker.Tcontent)) / + static_cast(ScrollingLines.Ticker.len)); - // pix change per scrolling line tick - ScrollingLines.SLine[j].dPix = - (static_cast(PixLengthLineIn - getDisplaySizeSettings(disp_resolution).Width)) / iScrollTime; + if (ScrollingLines.Ticker.TickerAvgPixPerChar < ScrollingLines.SLine[j].dPix) { + ScrollingLines.Ticker.TickerAvgPixPerChar = round(2 * ScrollingLines.SLine[j].dPix); + } + ScrollingLines.Ticker.MaxPixLen = getDisplaySizeSettings(disp_resolution).Width + 2 * ScrollingLines.Ticker.TickerAvgPixPerChar; + + // add more characters to display + while (true) { + char c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxEnd); + uint8_t PixForChar = display->getCharWidth(c); + + if ((ScrollingLines.SLine[0].Width + PixForChar) >= ScrollingLines.Ticker.MaxPixLen) { + break; // no more characters necessary to add + } + ScrollingLines.Ticker.IdxEnd++; + ScrollingLines.SLine[j].Width += PixForChar; + } +# endif // if P036_ENABLE_TICKER + } + else { + ScrollingLines.SLine[j].SLcontent = ScrollingPages.In[j].SPLcontent; + ScrollingLines.SLine[j].SLidx = ScrollingPages.In[j].SPLidx; // index to LineSettings[] + ScrollingLines.SLine[j].Width = PixLengthLineIn; // while page scrolling this line is left aligned + ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft; + ScrollingLines.SLine[j].fPixSum = getDisplaySizeSettings(disp_resolution).PixLeft; + + // pix change per scrolling line tick + ScrollingLines.SLine[j].dPix = + (static_cast(PixLengthLineIn - getDisplaySizeSettings(disp_resolution).Width)) / iScrollTime; + } # ifdef P036_SCROLL_CALC_LOG if (loglevelActiveFor(LOG_LEVEL_INFO)) { + delay(5); // otherwise it is may be to fast for the serial monitor String log; log.reserve(32); - log += F("Line: "); + log = F("Line: "); log += (j + 1); log += F(" width: "); log += ScrollingLines.SLine[j].Width; log += F(" dPix: "); log += ScrollingLines.SLine[j].dPix; addLogMove(LOG_LEVEL_INFO, log); +# if P036_ENABLE_TICKER + + if (bUseTicker) { + delay(5); // otherwise it is may be to fast for the serial monitor + String log1; + log1.reserve(200); + log1 = F("+++ iScrollTime: "); + log1 += iScrollTime; + log1 += F(" StrLength: "); + log1 += ScrollingLines.Ticker.len; + log1 += F(" StrInPix: "); + log1 += display->getStringWidth(ScrollingLines.Ticker.Tcontent); + log1 += F(" PixPerChar: "); + log1 += ScrollingLines.Ticker.TickerAvgPixPerChar; + addLogMove(LOG_LEVEL_INFO, log1); + } +# endif // if P036_ENABLE_TICKER } # endif // P036_SCROLL_CALC_LOG } @@ -1039,13 +1152,14 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(128)) { - log += F("Line: "); log += (j + 1); + delay(5); // otherwise it is may be to fast for the serial monitor + log = F("Line: "); log += (j + 1); log += F(" LineIn: "); log += LineInStr; log += F(" Length: "); log += strlen; log += F(" PixLength: "); log += PixLengthLineIn; log += F(" AvgPixPerChar: "); log += fAvgPixPerChar; log += F(" CharsRemoved: "); log += iCharToRemove; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); log.clear(); log += F(" -> Changed to: "); log += ScrollingPages.In[j].SPLcontent; log += F(" Length: "); log += ScrollingPages.In[j].SPLcontent.length(); @@ -1144,13 +1258,15 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(128)) { - log += F("Line: "); log += (j + 1); + delay(5); // otherwise it is may be to fast for the serial monitor + log = F("Line: "); log += (j + 1); log += F(" LineOut: "); log += LineOutStr; log += F(" Length: "); log += strlen; log += F(" PixLength: "); log += PixLengthLineOut; log += F(" AvgPixPerChar: "); log += fAvgPixPerChar; log += F(" CharsRemoved: "); log += iCharToRemove; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); + delay(5); // otherwise it is may be to fast for the serial monitor log.clear(); log += F(" -> Changed to: "); log += ScrollingPages.Out[j].SPLcontent; log += F(" Length: "); log += ScrollingPages.Out[j].SPLcontent.length(); @@ -1197,18 +1313,30 @@ uint8_t P036_data_struct::display_scroll_timer(bool initialScroll, } } - for (uint8_t j = 0; j < ScrollingPages.linesPerFrameOut; j++) { - if ((initialScroll && (lscrollspeed < ePageScrollSpeed::ePSS_Instant)) || - !initialScroll) { - // scrolling, prepare scrolling out to right - DrawScrollingPageLine(&ScrollingPages.Out[j], ScrollingLines.SLine[j].LastWidth, TEXT_ALIGN_RIGHT); + if (!bUseTicker) { + // for Ticker start with a black page + for (uint8_t j = 0; j < ScrollingPages.linesPerFrameOut; j++) { + if ((initialScroll && (lscrollspeed < ePageScrollSpeed::ePSS_Instant)) || + !initialScroll) { + // scrolling, prepare scrolling page out to right + DrawScrollingPageLine(&ScrollingPages.Out[j], ScrollingLines.SLine[j].LastWidth, TEXT_ALIGN_RIGHT); + } } - } - for (uint8_t j = 0; j < ScrollingPages.linesPerFrameIn; j++) { - // non-scrolling or scrolling prepare scrolling in from left - DrawScrollingPageLine(&ScrollingPages.In[j], ScrollingLines.SLine[j].Width, TEXT_ALIGN_LEFT); + for (uint8_t j = 0; j < ScrollingPages.linesPerFrameIn; j++) { + // non-scrolling or scrolling prepare scrolling page in from left + DrawScrollingPageLine(&ScrollingPages.In[j], ScrollingLines.SLine[j].Width, TEXT_ALIGN_LEFT); + } + } +# if P036_ENABLE_TICKER + else { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[0].SLidx].fontIdx].fontData); + display->drawString(ScrollingLines.SLine[0].CurrentLeft, + LineSettings[ScrollingLines.SLine[0].SLidx].ypos, + ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd)); } +# endif // if P036_ENABLE_TICKER update_display(); @@ -1244,9 +1372,8 @@ void P036_data_struct::display_scrolling_lines() { } if (bscroll) { - ScrollingLines.wait++; - if (ScrollingLines.wait < P36_WaitScrollLines) { + ScrollingLines.wait++; return; // wait before scrolling line not finished } @@ -1267,18 +1394,66 @@ void P036_data_struct::display_scrolling_lines() { display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[i].SLidx].fontIdx].fontData); - if (((ScrollingLines.SLine[i].CurrentLeft - getDisplaySizeSettings(disp_resolution).PixLeft) + - ScrollingLines.SLine[i].Width) >= getDisplaySizeSettings(disp_resolution).Width) { + if (bUseTicker || (((iCurrentLeft - getDisplaySizeSettings(disp_resolution).PixLeft) + + ScrollingLines.SLine[i].Width) >= getDisplaySizeSettings(disp_resolution).Width)) { display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(ScrollingLines.SLine[i].CurrentLeft, - LineSettings[ScrollingLines.SLine[i].SLidx].ypos, - ScrollingLines.SLine[i].SLcontent); + + if (bUseTicker) { +# if P036_ENABLE_TICKER + display->drawString(iCurrentLeft, + LineSettings[ScrollingLines.SLine[0].SLidx].ypos, + ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd)); + + // add more characters to display + while (true) { + if (ScrollingLines.Ticker.IdxEnd >= ScrollingLines.Ticker.len) { // end of string + break; + } + uint8_t c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxEnd); + uint8_t PixForChar = display->getCharWidth(c); // PixForChar can be 0 if c is non ascii + + if ((static_cast(ScrollingLines.SLine[0].Width + PixForChar) + iCurrentLeft) >= ScrollingLines.Ticker.MaxPixLen) { + break; // no more characters necessary to add + } + ScrollingLines.Ticker.IdxEnd++; + ScrollingLines.SLine[0].Width += PixForChar; + } + + // remove already displayed characters + while (ScrollingLines.SLine[0].fPixSum < (-2.0f * ScrollingLines.Ticker.TickerAvgPixPerChar)) { + uint8_t c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxStart); + uint8_t PixForChar = display->getCharWidth(c); // PixForChar can be 0 if c is non ascii + ScrollingLines.SLine[0].fPixSum += static_cast(PixForChar); + ScrollingLines.Ticker.IdxStart++; + + if (ScrollingLines.Ticker.IdxStart >= ScrollingLines.Ticker.IdxEnd) { + ScrollingLines.SLine[0].Width = 0; // Stop scrolling + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, F("Ticker finished")); + } + break; + } + + if (ScrollingLines.SLine[0].Width > PixForChar) { + ScrollingLines.SLine[0].Width -= PixForChar; + } + } + break; +# endif // if P036_ENABLE_TICKER + } else { + display->drawString(iCurrentLeft, + LineSettings[ScrollingLines.SLine[i].SLidx].ypos, + ScrollingLines.SLine[i].SLcontent); + } } else { - // line scrolling finished -> line is shown as aligned right - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(P36_MaxDisplayWidth - getDisplaySizeSettings(disp_resolution).PixLeft, - LineSettings[ScrollingLines.SLine[i].SLidx].ypos, - ScrollingLines.SLine[i].SLcontent); + if (!bUseTicker) { + // line scrolling finished -> line is shown as aligned right + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(P36_MaxDisplayWidth - getDisplaySizeSettings(disp_resolution).PixLeft, + LineSettings[ScrollingLines.SLine[i].SLidx].ypos, + ScrollingLines.SLine[i].SLcontent); + } ScrollingLines.SLine[i].Width = 0; // Stop scrolling } } @@ -1371,7 +1546,7 @@ void P036_data_struct::P036_JumpToPage(struct EventStruct *event, uint8_t nextFr P036_DisplayPage(event); // Display the selected page, // function needs // 65ms! - displayTimer = PCONFIG(4); // Restart timer + displayTimer = P036_TIMER; // Restart timer } void P036_data_struct::P036_JumpToPageOfLine(struct EventStruct *event, uint8_t LineNo) @@ -1380,6 +1555,9 @@ void P036_data_struct::P036_JumpToPageOfLine(struct EventStruct *event, uint8_t P036_JumpToPage(event, LineSettings[LineNo].DisplayedPageNo); } +// Defines the Scroll area layout +// Displays the selected page, function needs 65ms! +// Called by PLUGIN_READ and P036_JumpToPage() void P036_data_struct::P036_DisplayPage(struct EventStruct *event) { # ifdef PLUGIN_036_DEBUG @@ -1403,7 +1581,7 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) HeaderContentAlternative = static_cast(get8BitFromUL(PCONFIG_LONG(0), 0)); // Bit 7-0 // HeaderContentAlternative - // Construct the outgoing string + // Construct the outgoing string for (uint8_t i = 0; i < P36_Nlines; i++) { if (LineSettings[i].frame == frameCounter) { lineCounter = i; @@ -1449,7 +1627,7 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) frameCounter = nextFrameToDisplay; } - // Contruct incoming strings + // Contruct incoming strings for (uint8_t i = 0; i < P36_Nlines; i++) { if (nextFrameToDisplay == 0xff) { // showing next page @@ -1494,7 +1672,7 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) CalcMaxPageCount(); // Update max page count - // Update display + // Update display if (bDisplayingLogo) { bDisplayingLogo = false; display->clear(); // resets all pixels to black @@ -1508,12 +1686,13 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) update_display(); - bool bScrollWithoutWifi = bitRead(PCONFIG_LONG(0), 24); // Bit 24 - bool bScrollLines = bitRead(PCONFIG_LONG(0), 17); // Bit 17 - bLineScrollEnabled = (bScrollLines && (NetworkConnected() || bScrollWithoutWifi)); // scroll lines only if WifiIsConnected, + bool bScrollWithoutWifi = bitRead(PCONFIG_LONG(0), 24); // Bit 24 + bool bScrollLines = bitRead(PCONFIG_LONG(0), 17); // Bit 17 + bLineScrollEnabled = ((bScrollLines || bUseTicker) && (NetworkConnected() || bScrollWithoutWifi)); // scroll lines only if + // WifiIsConnected, // otherwise too slow - ePageScrollSpeed lscrollspeed = static_cast(PCONFIG(3)); + ePageScrollSpeed lscrollspeed = static_cast(P036_SCROLL); if (bPageScrollDisabled) { lscrollspeed = ePageScrollSpeed::ePSS_Instant; } // first page after INIT without scrolling @@ -1528,9 +1707,9 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) bPageScrollDisabled = false; // next PLUGIN_READ will do page scrolling } } else { - # ifdef PLUGIN_036_DEBUG + # ifdef PLUGIN_036_DEBUG addLog(LOG_LEVEL_INFO, F("P036_DisplayPage Display off")); - # endif // PLUGIN_036_DEBUG + # endif // PLUGIN_036_DEBUG } } @@ -1555,7 +1734,16 @@ String P036_data_struct::P36_parseTemplate(String& tmpString, uint8_t lineIdx) { uint32_t iAlignment = get3BitFromUL(LineContent->DisplayLinesV1[lineIdx].ModifyLayout, P036_FLAG_ModifyLayout_Alignment); - switch (getTextAlignment(static_cast(iAlignment))) { + OLEDDISPLAY_TEXT_ALIGNMENT iTextAlignment = getTextAlignment(static_cast(iAlignment)); + +# if P036_ENABLE_TICKER + + if (bUseTicker) { + iTextAlignment = TEXT_ALIGN_RIGHT; // ticker is always right aligned + } +# endif // if P036_ENABLE_TICKER + + switch (iTextAlignment) { case TEXT_ALIGN_LEFT: // add leading spaces from tmpString to the result @@ -1700,19 +1888,19 @@ void P036_data_struct::CalcMaxPageCount(void) { String log1; if (log1.reserve(140)) { // estimated - log1.clear(); log1 = F("CalcMaxPageCount: MaxFramesToDisplay:"); log1 += MaxFramesToDisplay; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); for (uint8_t i = 0; i < P36_Nlines; i++) { log1.clear(); - log1 += F("Line["); log1 += i; + delay(5); // otherwise it is may be to fast for the serial monitor + log1 = F("Line["); log1 += i; log1 += F("]: Frame:"); log1 += LineSettings[i].frame; log1 += F(" DisplayedPageNo:"); log1 += LineSettings[i].DisplayedPageNo; log1 += F(" FontIdx:"); log1 += LineSettings[i].fontIdx; log1 += F(" ypos:"); log1 += LineSettings[i].ypos - TopLineOffset; log1 += F(" FontHeight:"); log1 += LineSettings[i].FontHeight; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); } } } @@ -1774,29 +1962,35 @@ void P036_data_struct::DrawScrollingPageLine(tScrollingPageLines *ScrollingPageL } void P036_data_struct::CreateScrollingPageLine(tScrollingPageLines *ScrollingPageLine, uint8_t Counter) { - String tmpString(LineContent->DisplayLinesV1[Counter].Content); - - ScrollingPageLine->SPLcontent = P36_parseTemplate(tmpString, Counter); - - if (ScrollingPageLine->SPLcontent.length() > 0) { - int splitIdx = ScrollingPageLine->SPLcontent.indexOf("<|>"); // check for split token - - if (splitIdx >= 0) { - // split line into left and right part - tmpString = ScrollingPageLine->SPLcontent; - tmpString.replace(F("<|>"), " "); // replace in tmpString the split token with one space char - display->setFont(FontSizes[LineSettings[Counter].fontIdx].fontData); - uint16_t pixlength = display->getStringWidth(tmpString); // pixlength without split token but with one space char - tmpString = " "; - uint16_t charlength = display->getStringWidth(tmpString); // pix length for a space char - pixlength += charlength; - - while (pixlength <= getDisplaySizeSettings(disp_resolution).Width) { - // add more space chars until pixlength of the final line is almost the display width - tmpString += " "; // add another space char + if (bUseTicker) { +# if P036_ENABLE_TICKER + ScrollingPageLine->SPLcontent = EMPTY_STRING; +# endif // if P036_ENABLE_TICKER + } + else { + String tmpString(LineContent->DisplayLinesV1[Counter].Content); + ScrollingPageLine->SPLcontent = P36_parseTemplate(tmpString, Counter); + + if (ScrollingPageLine->SPLcontent.length() > 0) { + int splitIdx = ScrollingPageLine->SPLcontent.indexOf("<|>"); // check for split token + + if (splitIdx >= 0) { + // split line into left and right part + tmpString = ScrollingPageLine->SPLcontent; + tmpString.replace(F("<|>"), " "); // replace in tmpString the split token with one space char + display->setFont(FontSizes[LineSettings[Counter].fontIdx].fontData); + uint16_t pixlength = display->getStringWidth(tmpString); // pixlength without split token but with one space char + tmpString = " "; + uint16_t charlength = display->getStringWidth(tmpString); // pix length for a space char pixlength += charlength; + + while (pixlength <= getDisplaySizeSettings(disp_resolution).Width) { + // add more space chars until pixlength of the final line is almost the display width + tmpString += " "; // add another space char + pixlength += charlength; + } + ScrollingPageLine->SPLcontent.replace(F("<|>"), tmpString); // replace in final line the split token with space chars } - ScrollingPageLine->SPLcontent.replace(F("<|>"), tmpString); // replace in final line the split token with space chars } } uint32_t iAlignment = diff --git a/src/src/PluginStructs/P036_data_struct.h b/src/src/PluginStructs/P036_data_struct.h index 4c05e49e89..979f3cb0b2 100644 --- a/src/src/PluginStructs/P036_data_struct.h +++ b/src/src/PluginStructs/P036_data_struct.h @@ -25,13 +25,21 @@ // # define P036_CHECK_INDIVIDUAL_FONT // /Enable to add extra logging for individual font calculation # ifndef P036_LIMIT_BUILD_SIZE -# define P036_SEND_EVENTS // Enable sending events on Display On/Off, Contrast Low/Med/High, Frame and Line -# define P036_ENABLE_LINECOUNT // Enable the linecount subcommand +# define P036_SEND_EVENTS // Enable sending events on Display On/Off, Contrast Low/Med/High, Frame and Line +# define P036_ENABLE_LINECOUNT // Enable the linecount subcommand +# ifndef P036_ENABLE_TICKER +# define P036_ENABLE_TICKER 1 // Enable ticker function +# endif // ifndef +# else // ifndef P036_LIMIT_BUILD_SIZE +# ifndef P036_ENABLE_TICKER +# define P036_ENABLE_TICKER 0 // Disable ticker function +# endif // ifndef # endif // ifndef P036_LIMIT_BUILD_SIZE + # define P036_ENABLE_HIDE_FOOTER // Enable the Hide indicator (footer) option # define P036_ENABLE_LEFT_ALIGN // Enable the Left-align content option and leftalign subcommand -# define P36_Nlines 12 // The number of different lines which can be displayed - each line is 64 chars max +# define P36_Nlines 12 // The number of different lines which can be displayed - each line is 64 chars max # define P36_NcharsV0 32 // max chars per line up to 22.11.2019 (V0) # define P36_NcharsV1 64 // max chars per line from 22.11.2019 (V1) # define P36_MaxSizesCount 3 // number of different OLED sizes @@ -41,12 +49,12 @@ # define P36_MaxFontCount 5 // number of different fonts # endif // ifdef P036_LIMIT_BUILD_SIZE -# define P36_MaxDisplayWidth 128 +# define P36_MaxDisplayWidth 128 # define P36_MaxDisplayHeight 64 -# define P36_DisplayCentre 64 -# define P36_HeaderHeight 12 -# define P036_IndicatorTop 56 -# define P036_IndicatorHeight 8 +# define P36_DisplayCentre 64 +# define P36_HeaderHeight 12 +# define P036_IndicatorTop 56 +# define P036_IndicatorHeight 8 # define P36_WIFI_STATE_UNSET -2 # define P36_WIFI_STATE_NOT_CONNECTED -1 @@ -56,7 +64,7 @@ # define P36_PageScrollTick (P36_PageScrollTimer + 20) // total time for one PageScrollTick (including the handling time of 20ms // in PLUGIN_TIMER_IN) # define P36_PageScrollPix 4 // min pixel change while page scrolling -# define P36_DebounceTreshold 5 // number of 20 msec (fifty per second) ticks before the button has +# define P36_DebounceTreshold 5 // number of 20 msec (fifty per second) ticks before the button has // settled # define P36_RepeatDelay 50 // number of 20 msec ticks before repeating the button action when holding @@ -116,11 +124,12 @@ enum class p036_resolution { }; enum class ePageScrollSpeed { - ePSS_VerySlow = 1, // 800ms - ePSS_Slow = 2, // 400ms - ePSS_Fast = 4, // 200ms - ePSS_VeryFast = 8, // 100ms - ePSS_Instant = 32 // 20ms + ePSS_VerySlow = 1, // 800ms + ePSS_Slow = 2, // 400ms + ePSS_Fast = 4, // 200ms + ePSS_VeryFast = 8, // 100ms + ePSS_Instant = 32, // 20ms + ePSS_Ticker = 255u // tickerspeed depends on line length }; enum class eP036pinmode { @@ -138,15 +147,27 @@ typedef struct { uint8_t SLidx = 0; // index to DisplayLinesV1 } tScrollLine; +typedef struct { + String Tcontent; // content (all parsed lines) + uint16_t len = 0; // length of content + uint16_t IdxStart = 0; // Start index of TickerContent for displaying (left side) + uint16_t IdxEnd = 0; // End index of TickerContent for displaying (right side) + uint16_t TickerAvgPixPerChar = 0; // max of average pixel per character or pix change per scroll time (100ms) + int16_t MaxPixLen = 0; // Max pix length to display (display width + 2*TickerAvgPixPerChar) +} tTicker; + typedef struct { tScrollLine SLine[P36_MAX_LinesPerPage]{}; - uint16_t wait = 0; // waiting time before scrolling +# if P036_ENABLE_TICKER + tTicker Ticker; +# endif // if P036_ENABLE_TICKER + uint16_t wait = 0; // waiting time before scrolling } tScrollingLines; typedef struct { - String SPLcontent; // content + String SPLcontent; // content OLEDDISPLAY_TEXT_ALIGNMENT Alignment = TEXT_ALIGN_LEFT; - uint8_t SPLidx = 0; // index to DisplayLinesV1 + uint8_t SPLidx = 0; // index to DisplayLinesV1 } tScrollingPageLines; typedef struct { @@ -154,7 +175,7 @@ typedef struct { tScrollingPageLines Out[P36_MAX_LinesPerPage]{}; int dPixSum = 0; // act pix change uint8_t Scrolling = 0; // 0=Ready, 1=Scrolling - uint8_t dPix = 0; // pix change per scroll time (25ms) + uint8_t dPix = 0; // pix change per scroll time (25ms per page, 100ms per line) uint8_t linesPerFrameDef = 0; // the default number of lines in frame in/out uint8_t linesPerFrameIn = 0; // the number of lines in frame in uint8_t linesPerFrameOut = 0; // the number of lines in frame out @@ -229,9 +250,9 @@ typedef struct { typedef struct { uint8_t fontIdx = 0; // font index for this line setting - uint8_t Top = 0; // top in pix for this line setting - uint8_t Height = 0; // font height in pix - int8_t Space = 0; // space in pix between lines for this line setting, allow negative values to squeeze the lines closer! + uint8_t Top = 0; // top in pix for this line setting + uint8_t Height = 0; // font height in pix + int8_t Space = 0; // space in pix between lines for this line setting, allow negative values to squeeze the lines closer! # ifdef P036_FONT_CALC_LOG const __FlashStringHelper* FontName() const; # endif // ifdef P036_FONT_CALC_LOG @@ -247,15 +268,15 @@ typedef struct { } tSizeSettings; typedef struct { - uint8_t frame = 0; // frame for this line + uint8_t frame = 0; // frame for this line uint8_t DisplayedPageNo = 0; // number of shown pages for this line, set in CalcMaxPageCount() - uint8_t ypos = 0; // ypos for this line - uint8_t fontIdx = 0; // font index for this line - uint8_t FontHeight = 0; // font height for this line + uint8_t ypos = 0; // ypos for this line + uint8_t fontIdx = 0; // font index for this line + uint8_t FontHeight = 0; // font height for this line } tLineSettings; typedef struct { - uint8_t NextLineNo = 0; // number of next line or 0xFF if settings do not fit + uint8_t NextLineNo = 0; // number of next line or 0xFF if settings do not fit uint8_t IdxForBiggestFontUsed = 0; // ypos for this line } tIndividualFontSettings; @@ -284,17 +305,18 @@ struct P036_data_struct : public PluginTaskData_base { static const tSizeSettings& getDisplaySizeSettings(p036_resolution disp_resolution); - bool init(taskIndex_t taskIndex, - uint8_t LoadVersion, - uint8_t Type, - uint8_t Address, - uint8_t Sda, - uint8_t Scl, - p036_resolution Disp_resolution, - bool Rotated, - uint8_t Contrast, - uint16_t DisplayTimer, - uint8_t NrLines); + bool init(taskIndex_t taskIndex, + uint8_t LoadVersion, + uint8_t Type, + uint8_t Address, + uint8_t Sda, + uint8_t Scl, + p036_resolution Disp_resolution, + bool Rotated, + uint8_t Contrast, + uint16_t DisplayTimer, + ePageScrollSpeed ScrollSpeed, + uint8_t NrLines); bool isInitialized() const; @@ -306,9 +328,16 @@ struct P036_data_struct : public PluginTaskData_base { void setOrientationRotated(bool rotated); # ifdef P036_ENABLE_LINECOUNT - void setNrLines(uint8_t NrLines); + void setNrLines(struct EventStruct *event, + uint8_t NrLines); # endif // P036_ENABLE_LINECOUNT + // Restores line content from flash memory + // LineNo == 0: all line contents + // otherwise just the line content of the given LineNo + void RestoreLineContent(taskIndex_t taskIndex, + uint8_t LoadVersion, + uint8_t LineNo); // The screen is set up as: // - 10 rows at the top for the header @@ -319,7 +348,8 @@ struct P036_data_struct : public PluginTaskData_base { void display_title(const String& title); void display_logo(); void display_indicator(); - void prepare_pagescrolling(); + void prepare_pagescrolling(ePageScrollSpeed lscrollspeed, + uint8_t NrLines); uint8_t display_scroll(ePageScrollSpeed lscrollspeed, int lTaskTimer); uint8_t display_scroll_timer(bool initialScroll = false, @@ -364,8 +394,8 @@ struct P036_data_struct : public PluginTaskData_base { // Instantiate display here - does not work to do this within the INIT call OLEDDisplay *display = nullptr; - tScrollingLines ScrollingLines{}; - tScrollingPages ScrollingPages{}; + tScrollingLines ScrollingLines{}; // scrolling lines in from right, out to left + tScrollingPages ScrollingPages{}; // scrolling pages in from left, out to right // CustomTaskSettings P036_LineContent *LineContent = nullptr; @@ -394,12 +424,13 @@ struct P036_data_struct : public PluginTaskData_base { bool bReduceLinesPerFrame = false; // frames - uint8_t MaxFramesToDisplay = 0; // total number of frames to display + uint8_t MaxFramesToDisplay = 0; // total number of frames to display uint8_t currentFrameToDisplay = 0; - uint8_t nextFrameToDisplay = 0; // next frame because content changed in PLUGIN_WRITE - uint8_t frameCounter = 0; // need to keep track of framecounter from call to call - uint8_t disableFrameChangeCnt = 0; // counter to disable frame change after JumpToPage in case PLUGIN_READ already scheduled - bool bPageScrollDisabled = true; // first page after INIT or after JumpToPage without scrolling + uint8_t nextFrameToDisplay = 0; // next frame because content changed in PLUGIN_WRITE + uint8_t frameCounter = 0; // need to keep track of framecounter from call to call + uint8_t disableFrameChangeCnt = 0; // counter to disable frame change after JumpToPage in case PLUGIN_READ already scheduled + bool bPageScrollDisabled = true; // first page after INIT or after JumpToPage without scrolling + bool bUseTicker = false; // scroll line like a ticker OLEDDISPLAY_TEXT_ALIGNMENT textAlignment = TEXT_ALIGN_CENTER; From f958b0a3d38962dcada4501f4319a8a38b77438e Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Mon, 24 Jul 2023 17:16:46 +0200 Subject: [PATCH 2/9] Compiler error if P036_SendEvent was not set --- src/_P036_FrameOLED.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index 77e82ba740..836e9492bc 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -191,7 +191,6 @@ # define PLUGIN_NAME_036 "Display - OLED SSD1306/SH1106 Framed" # define PLUGIN_VALUENAME1_036 "OLED" -# ifdef P036_SEND_EVENTS # define P036_EVENT_DISPLAY 0 // event: #display=0/1 # define P036_EVENT_CONTRAST 1 // event: #contrast=0/1/2 # define P036_EVENT_FRAME 2 // event: #frame=1..n @@ -199,6 +198,7 @@ # define P036_EVENT_LINECNT 4 // event: #linecount=1..4 # define P036_EVENT_RESTORE 5 // event: #restore=1..n # define P036_EVENT_SCROLL 6 // event: #scroll=ePSS_VerySlow..ePSS_Ticker +# ifdef P036_SEND_EVENTS void P036_SendEvent(struct EventStruct *event, uint8_t eventId, int16_t eventValue); From 639fa9de5b8fe05c57c02ce47a8d2ee0b09ec3d7 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Tue, 25 Jul 2023 21:49:01 +0200 Subject: [PATCH 3/9] Calculation for ticker IdxStart and IdxEnd was wrong for 64x48 display, Start page updates after network has connected in PLUGIN_ONCE_A_SECOND BUG: Calculation for ticker IdxStart and IdxEnd was wrong for 64x48 display CHG: Start page updates after network has connected in PLUGIN_ONCE_A_SECOND, faster than waiting for the next PLUGIN_READ --- src/_P036_FrameOLED.ino | 28 +++++++++++++++------- src/src/PluginStructs/P036_data_struct.cpp | 13 ++++++---- src/src/PluginStructs/P036_data_struct.h | 1 + 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index 836e9492bc..1b8510c676 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -14,6 +14,9 @@ // Added to the main repository with some optimizations and some limitations. // Al long as the device is not selected, no RAM is waisted. // +// @uwekaditz: 2023-07-25 +// BUG: Calculation for ticker IdxStart and IdxEnd was wrong for 64x48 display +// CHG: Start page updates after network has connected in PLUGIN_ONCE_A_SECOND, faster than waiting for the next PLUGIN_READ // @uwekaditz: 2023-07-23 // NEW: Add ticker for scrolling speed, solves issue #4188 // ADD: Setting and support for oledframedcmd,restore,<0|> subcommand par2: (0=all|Line Content) @@ -181,6 +184,7 @@ // CHG: Parameters sorted +# include "src/ESPEasyCore/ESPEasyNetwork.h" # include "src/PluginStructs/P036_data_struct.h" # ifdef P036_CHECK_HEAP # include "src/Helpers/Memory.h" @@ -891,15 +895,22 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) if (P036_DisplayIsOn) { // Display is on. - P036_data->HeaderContent = static_cast(get8BitFromUL(P036_FLAGS_0, P036_FLAG_HEADER)); // HeaderContent - P036_data->HeaderContentAlternative = static_cast(get8BitFromUL(P036_FLAGS_0, P036_FLAG_HEADER_ALTERNATIVE)); + if (!P036_data->bRunning && NetworkConnected() && (P036_data->ScrollingPages.Scrolling == 0)) { + // start page updates after network has connected + P036_data->P036_DisplayPage(event); + } + else { + + P036_data->HeaderContent = static_cast(get8BitFromUL(P036_FLAGS_0, P036_FLAG_HEADER));// HeaderContent + P036_data->HeaderContentAlternative = static_cast(get8BitFromUL(P036_FLAGS_0, P036_FLAG_HEADER_ALTERNATIVE)); - // HeaderContentAlternative - P036_data->display_header(); // Update Header + // HeaderContentAlternative + P036_data->display_header();// Update Header - if (P036_data->isInitialized() && P036_data->display_wifibars()) { - // WiFi symbol was updated. - P036_data->update_display(); + if (P036_data->isInitialized() && P036_data->display_wifibars()) { + // WiFi symbol was updated. + P036_data->update_display(); + } } } @@ -1159,9 +1170,8 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) get4BitFromUL(P036_FLAGS_0, P036_FLAG_SETTINGS_VERSION), // Bit23-20 Version CustomTaskSettings LineNo); - if (LineNo == 0) { + if (LineNo == 0) LineNo = 1; // after restoring all contents start with first Line - } eventId = P036_EVENT_RESTORE; bUpdateDisplay = true; } diff --git a/src/src/PluginStructs/P036_data_struct.cpp b/src/src/PluginStructs/P036_data_struct.cpp index ba5d00ab36..7e920dd5b6 100644 --- a/src/src/PluginStructs/P036_data_struct.cpp +++ b/src/src/PluginStructs/P036_data_struct.cpp @@ -260,6 +260,8 @@ bool P036_data_struct::init(taskIndex_t taskIndex, prepare_pagescrolling(ScrollSpeed, NrLines); } + bRunning = NetworkConnected(); + return isInitialized(); } @@ -1314,7 +1316,6 @@ uint8_t P036_data_struct::display_scroll_timer(bool initialScroll, } if (!bUseTicker) { - // for Ticker start with a black page for (uint8_t j = 0; j < ScrollingPages.linesPerFrameOut; j++) { if ((initialScroll && (lscrollspeed < ePageScrollSpeed::ePSS_Instant)) || !initialScroll) { @@ -1330,6 +1331,7 @@ uint8_t P036_data_struct::display_scroll_timer(bool initialScroll, } # if P036_ENABLE_TICKER else { + // for Ticker start with the set alignment display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[0].SLidx].fontIdx].fontData); display->drawString(ScrollingLines.SLine[0].CurrentLeft, @@ -1405,6 +1407,7 @@ void P036_data_struct::display_scrolling_lines() { ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd)); // add more characters to display + iCurrentLeft -= getDisplaySizeSettings(disp_resolution).PixLeft; while (true) { if (ScrollingLines.Ticker.IdxEnd >= ScrollingLines.Ticker.len) { // end of string break; @@ -1420,7 +1423,8 @@ void P036_data_struct::display_scrolling_lines() { } // remove already displayed characters - while (ScrollingLines.SLine[0].fPixSum < (-2.0f * ScrollingLines.Ticker.TickerAvgPixPerChar)) { + float fCurrentPixLeft = static_cast(getDisplaySizeSettings(disp_resolution).PixLeft) - 2.0f * ScrollingLines.Ticker.TickerAvgPixPerChar; + while (ScrollingLines.SLine[0].fPixSum < fCurrentPixLeft) { uint8_t c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxStart); uint8_t PixForChar = display->getCharWidth(c); // PixForChar can be 0 if c is non ascii ScrollingLines.SLine[0].fPixSum += static_cast(PixForChar); @@ -1688,7 +1692,8 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) bool bScrollWithoutWifi = bitRead(PCONFIG_LONG(0), 24); // Bit 24 bool bScrollLines = bitRead(PCONFIG_LONG(0), 17); // Bit 17 - bLineScrollEnabled = ((bScrollLines || bUseTicker) && (NetworkConnected() || bScrollWithoutWifi)); // scroll lines only if + bRunning = NetworkConnected() || bScrollWithoutWifi; + bLineScrollEnabled = ((bScrollLines || bUseTicker) && bRunning);// scroll lines only if // WifiIsConnected, // otherwise too slow @@ -1702,7 +1707,7 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) Scheduler.setPluginTaskTimer(P36_PageScrollTimer, event->TaskIndex, event->Par1); // calls next page scrollng tick } - if (NetworkConnected() || bScrollWithoutWifi) { + if (bRunning) { // scroll lines only if WifiIsConnected, otherwise too slow bPageScrollDisabled = false; // next PLUGIN_READ will do page scrolling } diff --git a/src/src/PluginStructs/P036_data_struct.h b/src/src/PluginStructs/P036_data_struct.h index 979f3cb0b2..479487bfdc 100644 --- a/src/src/PluginStructs/P036_data_struct.h +++ b/src/src/PluginStructs/P036_data_struct.h @@ -430,6 +430,7 @@ struct P036_data_struct : public PluginTaskData_base { uint8_t frameCounter = 0; // need to keep track of framecounter from call to call uint8_t disableFrameChangeCnt = 0; // counter to disable frame change after JumpToPage in case PLUGIN_READ already scheduled bool bPageScrollDisabled = true; // first page after INIT or after JumpToPage without scrolling + bool bRunning = false; // page updates are rumming = (NetworkConnected() || bScrollWithoutWifi) bool bUseTicker = false; // scroll line like a ticker OLEDDISPLAY_TEXT_ALIGNMENT textAlignment = TEXT_ALIGN_CENTER; From 87afa6142a8bd950fec71f78f981b5d8183424d0 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Thu, 10 Aug 2023 22:37:10 +0200 Subject: [PATCH 4/9] Some bug fixes (only 1 line displayed) - Individual font setting can only enlarge or maximize the font, if more than 1 line should be displayed (it was buggy not only for ticker!) - CalculateIndividualFontSettings() must be called until the font fits (it was buggy not only for ticker!) - Compiler error for '#ifdef P036_FONT_CALC_LOG' --- src/_P036_FrameOLED.ino | 5 +- src/src/PluginStructs/P036_data_struct.cpp | 68 +++++++++++++++------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index 1b8510c676..1c955fb1f9 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -13,7 +13,10 @@ // Major work on this plugin has been done by 'Namirda' // Added to the main repository with some optimizations and some limitations. // Al long as the device is not selected, no RAM is waisted. -// +// @uwekaditz: 2023-08-10 +// BUG: Individual font setting can only enlarge or maximize the font, if more than 1 line should be displayed (it was buggy not only for ticker!) +// BUG: CalculateIndividualFontSettings() must be called until the font fits (it was buggy not only for ticker!) +// BUG: Compiler error for '#ifdef P036_FONT_CALC_LOG' // @uwekaditz: 2023-07-25 // BUG: Calculation for ticker IdxStart and IdxEnd was wrong for 64x48 display // CHG: Start page updates after network has connected in PLUGIN_ONCE_A_SECOND, faster than waiting for the next PLUGIN_READ diff --git a/src/src/PluginStructs/P036_data_struct.cpp b/src/src/PluginStructs/P036_data_struct.cpp index 12702c4a87..536447fb0c 100644 --- a/src/src/PluginStructs/P036_data_struct.cpp +++ b/src/src/PluginStructs/P036_data_struct.cpp @@ -577,14 +577,20 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ switch (static_cast(iModifyFont)) { case eModifyFont::eEnlarge: - lFontIndex -= 1; + if (ScrollingPages.linesPerFrameDef > 1) { + // Font can only be enlarged if more than 1 line is displayed + lFontIndex -= 1; - if (lFontIndex < IdxForBiggestFont) { lFontIndex = IdxForBiggestFont; } - result.IdxForBiggestFontUsed = lFontIndex; + if (lFontIndex < IdxForBiggestFont) { lFontIndex = IdxForBiggestFont; } + result.IdxForBiggestFontUsed = lFontIndex; + } break; case eModifyFont::eMaximize: - lFontIndex = IdxForBiggestFont; - result.IdxForBiggestFontUsed = lFontIndex; + if (ScrollingPages.linesPerFrameDef > 1) { + // Font can only be maximized if more than 1 line is displayed + lFontIndex = IdxForBiggestFont; + result.IdxForBiggestFontUsed = lFontIndex; + } break; case eModifyFont::eReduce: lFontIndex += 1; @@ -618,6 +624,9 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ // just one lines per frame -> no space inbetween lSpace = 0; lTop = (MaxHeight - lHeight) / 2; + if (lHeight > MaxHeight) { + result.NextLineNo = 0xFF; // settings do not fit + } } else { if (deltaHeight >= (lLinesPerFrame - 1)) { // individual line setting fits @@ -658,6 +667,28 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ LineSettings[k].ypos = LineSettings[k - 1].ypos + FontSizes[LineSettings[k - 1].fontIdx].Height + lSpace; } } +# ifdef P036_CHECK_INDIVIDUAL_FONT + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log1; + + if (log1.reserve(140)) { // estimated + delay(10); // otherwise it is may be to fast for the serial monitor + log1.clear(); + log1 = F("IndividualFontSettings:"); + log1 += F(" result.NextLineNo:"); log1 += result.NextLineNo; + log1 += F(" result.IdxForBiggestFontUsed:"); log1 += result.IdxForBiggestFontUsed; + log1 += F(" LineNo:"); log1 += LineNo; + log1 += F(" LinesPerFrame:"); log1 += LinesPerFrame; + if (result.NextLineNo != 0xFF) { + log1 += F(" FrameNo:"); log1 += FrameNo; + log1 += F(" lTop:"); log1 += lTop; + log1 += F(" lSpace:"); log1 += lSpace; + } + addLogMove(LOG_LEVEL_INFO, log1); + } + } +#endif // # ifdef P036_CHECK_INDIVIDUAL_FONT return result; } @@ -687,8 +718,8 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { strformat(F("P036 CalculateFontSettings lines: %d, height: %d, header: %s, footer: %s"), iLinesPerFrame, iHeight, - boolToString(!bHideHeader).c_str(), - boolToString(!bHideFooter).c_str())); + boolToString(!bHideHeader), + boolToString(!bHideFooter))); } # endif // ifdef P036_FONT_CALC_LOG @@ -806,8 +837,8 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { if (IndividualFontSettings.NextLineNo == 0xFF) { // individual settings do not fit - if (bReduceLinesPerFrame) { - currentLinesPerFrame--; // reduce numer of lines per frame + if ((bReduceLinesPerFrame) && (currentLinesPerFrame > 1)) { + currentLinesPerFrame--; // reduce number of lines per frame } else { iIdxForBiggestFont = IndividualFontSettings.IdxForBiggestFontUsed + 1; // use smaller font size as maximum } @@ -826,17 +857,6 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { String log1; if (log1.reserve(140)) { // estimated - delay(5); // otherwise it is may be to fast for the serial monitor - log1.clear(); - log1 = F("IndividualFontSettings:"); - log1 += F(" iFontIndex:"); log1 += iFontIndex; - log1 += F(" iLinesPerFrame:"); log1 += iLinesPerFrame; - log1 += F(" TopLineOffset:"); log1 += TopLineOffset; - log1 += F(" iHeight:"); log1 += iHeight; - log1 += F(" iUsedHeightForFonts:"); log1 += iUsedHeightForFonts; - log1 += F(" iMaxHeightForFont:"); log1 += iMaxHeightForFont; - addLogMove(LOG_LEVEL_INFO, log1); - for (uint8_t i = 0; i < P36_Nlines; i++) { delay(5); // otherwise it is may be to fast for the serial monitor log1.clear(); @@ -1397,6 +1417,7 @@ void P036_data_struct::display_scrolling_lines() { // add more characters to display iCurrentLeft -= getDisplaySizeSettings(disp_resolution).PixLeft; + while (true) { if (ScrollingLines.Ticker.IdxEnd >= ScrollingLines.Ticker.len) { // end of string break; @@ -1412,7 +1433,9 @@ void P036_data_struct::display_scrolling_lines() { } // remove already displayed characters - float fCurrentPixLeft = static_cast(getDisplaySizeSettings(disp_resolution).PixLeft) - 2.0f * ScrollingLines.Ticker.TickerAvgPixPerChar; + float fCurrentPixLeft = static_cast(getDisplaySizeSettings(disp_resolution).PixLeft) - 2.0f * + ScrollingLines.Ticker.TickerAvgPixPerChar; + while (ScrollingLines.SLine[0].fPixSum < fCurrentPixLeft) { uint8_t c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxStart); uint8_t PixForChar = display->getCharWidth(c); // PixForChar can be 0 if c is non ascii @@ -1422,9 +1445,12 @@ void P036_data_struct::display_scrolling_lines() { if (ScrollingLines.Ticker.IdxStart >= ScrollingLines.Ticker.IdxEnd) { ScrollingLines.SLine[0].Width = 0; // Stop scrolling +# ifdef PLUGIN_036_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, F("Ticker finished")); } +# endif // PLUGIN_036_DEBUG break; } From 4f43b2fcef8605f26b46de90bd99c72f98c987a3 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Fri, 11 Aug 2023 20:19:20 +0200 Subject: [PATCH 5/9] Removed unnecessary clear() functions --- src/src/PluginStructs/P036_data_struct.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/src/PluginStructs/P036_data_struct.cpp b/src/src/PluginStructs/P036_data_struct.cpp index 536447fb0c..0f1f94e5d7 100644 --- a/src/src/PluginStructs/P036_data_struct.cpp +++ b/src/src/PluginStructs/P036_data_struct.cpp @@ -674,7 +674,6 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ if (log1.reserve(140)) { // estimated delay(10); // otherwise it is may be to fast for the serial monitor - log1.clear(); log1 = F("IndividualFontSettings:"); log1 += F(" result.NextLineNo:"); log1 += result.NextLineNo; log1 += F(" result.IdxForBiggestFontUsed:"); log1 += result.IdxForBiggestFontUsed; @@ -738,7 +737,6 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { # ifdef P036_FONT_CALC_LOG String log1; log1.reserve(80); - log1.clear(); # endif // ifdef P036_FONT_CALC_LOG for (i = 0; i < P36_MaxFontCount - 1; i++) { @@ -881,7 +879,6 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { if (log1.reserve(140)) { // estimated delay(5); // otherwise it is may be to fast for the serial monitor - log1.clear(); log1 = F("CalculateFontSettings: Font:"); log1 += result.FontName(); log1 += F(" Idx:"); From dccd13f0f198c5cbe4c6f1237a269472a45fdfc5 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Sun, 27 Aug 2023 16:13:14 +0200 Subject: [PATCH 6/9] Merged with mega from 2023-08-25 --- docs/source/Plugin/P036.rst | 14 +- docs/source/Plugin/P036_ScrollOptions.png | Bin 24420 -> 29566 bytes docs/source/Plugin/P036_commands.repl | 16 + lib/esp8266-oled-ssd1306/OLEDDisplay.cpp | 9 + lib/esp8266-oled-ssd1306/OLEDDisplay.h | 5 + src/Custom-sample.h | 1 + src/_P036_FrameOLED.ino | 203 +++++++--- src/src/PluginStructs/P036_data_struct.cpp | 434 ++++++++++++++++----- src/src/PluginStructs/P036_data_struct.h | 46 ++- 9 files changed, 554 insertions(+), 174 deletions(-) diff --git a/docs/source/Plugin/P036.rst b/docs/source/Plugin/P036.rst index 6dd1183fc1..2be3e553ca 100644 --- a/docs/source/Plugin/P036.rst +++ b/docs/source/Plugin/P036.rst @@ -65,7 +65,17 @@ Device Settings .. image:: P036_ScrollOptions.png -* **Scroll**: Switching between pages can be "instant" or "scrolling". Please note that scrolling will need more resources of the ESP, which can have an effect on other active tasks of the node. +* **Scroll**: Switching between pages can be "instant", "scrolling" or a "ticker" band. Please note that scrolling will need more resources of the ESP, which can have an effect on other active tasks of the node. +For the ``Ticker`` there are some restrictions: +* Depending on the build used (NORMAL and CUSTOM) this option is available +* only one line is displaying the ticker string +* all line contents are parsed and combined to this ticker string, the parsing happens only at each ticker start (using the setting ``Interval``) +* the optional split token ``<|>`` is replaced by three spaces +* the gaps between the ticker items must be set by the ``line content`` (tailing spaces) +* the starting alignment (left, center, right) depends on the setting ``Align content (global)`` +* the font is taken from the setting ``Modify font`` of the first line, the ``Alignment`` settings of the lines are ignored (always right aligned) +* the ticker speed depends on the length of the ticker string and the setting ``Interval`` setting +* the footer is automatically hidden * **GPIO <- Display button**: Setting up a ``Display Button``, allows to configure a Display Timeout and wake the display on demand, either by a button, or by using some presence detection. @@ -125,7 +135,7 @@ The user defined texts may also contain a split token ``<|>`` to display the lin * **Alignment**: For each line, the alignment to be used can be selected, or the global setting can be used. -* **Interval** By default, Interval will be set to 0. If set to a non-zero value, the pre-configured content will be updated automatically using that interval (seconds). +* **Interval** By default, Interval will be set to 0. It needs to be set to a non-zero value, to switch between the frames using that interval (seconds). General diff --git a/docs/source/Plugin/P036_ScrollOptions.png b/docs/source/Plugin/P036_ScrollOptions.png index cfed679c78caa5fd366c3ed15913dca885cc75a1..c549d93db47de2f25eca157b1fce425f9b1247d7 100644 GIT binary patch literal 29566 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 diff --git a/docs/source/Plugin/P036_commands.repl b/docs/source/Plugin/P036_commands.repl index 97a056f4e3..c02a834c34 100644 --- a/docs/source/Plugin/P036_commands.repl +++ b/docs/source/Plugin/P036_commands.repl @@ -39,6 +39,7 @@ The updated line text is not stored in the settings itself, but kept in memory. After a reboot the stored plugin settings will be used. + The line text can also be restored from the settings by the command ``restore``. All template notations can be used, like system variables, or reference to a task value. @@ -50,6 +51,7 @@ This command is to display a specific frame (aka page), or the next frame. When reaching the last frame, a 'next' (0) will display the first frame. The parameter corresponds to the desired frame (1..) to display. The number of frames is determined by dividing the lines in use (at least one line in that frame with some data), by the number of Lines per Frame. So practically, the range is 1..3 when all lines are used and 4 Lines per Frame is set, or 1..12 if Line per frames is set to 1. The number of frames is updated if a frame would initially be empty, and an external source places text on a line of that frame (see above). + If scroll is set to ``ticker`` only = 1 is supported, it starts the ticker from the beginning. When omitting , or providing 0, the next frame is displayed. @@ -59,6 +61,7 @@ ``oledframedcmd,linecount,<1..4>`` "," This command changes the number of lines in each frame. When the next frame is to be displayed, the frames are recalculated and the sequence is restarted at the first frame. + If scroll is set to ``ticker`` this command is not supported. If Generate events for 'Linecount' is selected, a ```` event is generated on initialization of the plugin and when changing the setting. " @@ -73,6 +76,19 @@ Set the global align option for content to centre (0), left (1) or right (2). " " + ``oledframedcmd,restore,`` + "," + If the parameter is set to 0 all line contents will be restored from the settings. + Otherwise the parameter corresponds with the same lines as the plugin configuration has, + and only the content of this line will be restored from the settings. + " + " + ``oledframedcmd,scroll,`` + "," + The parameter corresponds with the line number of the scroll parameter of the settings (1=Very slow ... 6=Ticker). + After applying the new scroll speed the display restarts with the first page. + " + " ``oledframedcmd,userDef1,""`` "," Set the user defined header nr. 1 with any desired text value. diff --git a/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp b/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp index 40986a3d19..8978c48fe7 100644 --- a/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp +++ b/lib/esp8266-oled-ssd1306/OLEDDisplay.cpp @@ -532,6 +532,15 @@ uint16_t OLEDDisplay::getStringWidth(const String& strUser) { return width; } +uint8_t OLEDDisplay::getCharWidth(const char c) { + uint8_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + if (utf8ascii(c) == 0) + return 0; + if (c < firstChar) + return 0; + return pgm_read_byte(fontData + JUMPTABLE_START + (c- firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); +} + void OLEDDisplay::setTextAlignment(OLEDDISPLAY_TEXT_ALIGNMENT textAlignment) { this->textAlignment = textAlignment; } diff --git a/lib/esp8266-oled-ssd1306/OLEDDisplay.h b/lib/esp8266-oled-ssd1306/OLEDDisplay.h index 978fc9cc10..16433c0a64 100644 --- a/lib/esp8266-oled-ssd1306/OLEDDisplay.h +++ b/lib/esp8266-oled-ssd1306/OLEDDisplay.h @@ -190,6 +190,11 @@ class OLEDDisplay : public Print { // Convencience method for the const char version uint16_t getStringWidth(const String& text); + // Returns the width of c with the already set fontData + // returns a 0 if c is non-ascii + // in this case the next char must be converted + uint8_t getCharWidth(const char c); + // Specifies relative to which anchor point // the text is rendered. Available constants: // TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER_BOTH diff --git a/src/Custom-sample.h b/src/Custom-sample.h index e040e63de2..012cd7674c 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -378,6 +378,7 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = { // #define USES_P036 // FrameOLED // #define P036_FEATURE_DISPLAY_PREVIEW 1 // Enable Preview feature, shows on-display content on Devices overview page // #define P036_FEATURE_ALIGN_PREVIEW 1 // Enable center/right-align feature when preview is enabled (auto-disabled for 1M builds) +// #define P036_ENABLE_TICKER 1 // Enable ticker function // #define USES_P037 // MQTTImport // #define P037_MAPPING_SUPPORT 1 // Enable Value mapping support // #define P037_FILTER_SUPPORT 1 // Enable filtering support diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index 65636101b3..7c16d3fe50 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -14,8 +14,20 @@ // Added to the main repository with some optimizations and some limitations. // As long as the device is not enabled, no RAM is wasted. // +// @uwekaditz: 2023-08-10 +// BUG: Individual font setting can only enlarge or maximize the font, if more than 1 line should be displayed (it was buggy not only for ticker!) +// BUG: CalculateIndividualFontSettings() must be called until the font fits (it was buggy not only for ticker!) +// BUG: Compiler error for '#ifdef P036_FONT_CALC_LOG' // @tonhuisman: 2023-08-08 // CHG: Enable Userdefined headers feature, even on LIMIT_BUILD_SIZE builds +// @uwekaditz: 2023-07-25 +// BUG: Calculation for ticker IdxStart and IdxEnd was wrong for 64x48 display +// CHG: Start page updates after network has connected in PLUGIN_ONCE_A_SECOND, faster than waiting for the next PLUGIN_READ +// @uwekaditz: 2023-07-23 +// NEW: Add ticker for scrolling speed, solves issue #4188 +// ADD: Setting and support for oledframedcmd,restore,<0|> subcommand par2: (0=all|Line Content) +// ADD: Setting and support for oledframedcmd,scroll,<1..6> subcommand, par2: (casted to ePageScrollSpeeds) +// CHG: Minor change in debug messages (addLogMove() for dynamic messages) // @tonhuisman: 2023-07-01 // CHG: Make compile-time defines for P036_SEND_EVENTS boolean // CHG: Make compile-time defines for P036_ENABLE_LINECOUNT boolean @@ -207,12 +219,14 @@ # define PLUGIN_NAME_036 "Display - OLED SSD1306/SH1106 Framed" # define PLUGIN_VALUENAME1_036 "OLED" -# if P036_SEND_EVENTS # define P036_EVENT_DISPLAY 0 // event: #display=0/1 # define P036_EVENT_CONTRAST 1 // event: #contrast=0/1/2 # define P036_EVENT_FRAME 2 // event: #frame=1..n # define P036_EVENT_LINE 3 // event: #line=1..n # define P036_EVENT_LINECNT 4 // event: #linecount=1..4 +# define P036_EVENT_RESTORE 5 // event: #restore=1..n +# define P036_EVENT_SCROLL 6 // event: #scroll=ePSS_VerySlow..ePSS_Ticker +# if P036_SEND_EVENTS void P036_SendEvent(struct EventStruct *event, uint8_t eventId, int16_t eventValue); @@ -321,20 +335,32 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } # endif // if P036_ENABLE_LEFT_ALIGN { - const __FlashStringHelper *options[5] = { +# if P036_ENABLE_TICKER + const int optionCnt = 6; +# else // if P036_ENABLE_TICKER + const int optionCnt = 5; +# endif // if P036_ENABLE_TICKER + const __FlashStringHelper *options[optionCnt] = { F("Very Slow"), F("Slow"), F("Fast"), F("Very Fast"), - F("Instant") + F("Instant"), +# if P036_ENABLE_TICKER + F("Ticker"), +# endif // if P036_ENABLE_TICKER }; - const int optionValues[5] = + const int optionValues[optionCnt] = { static_cast(ePageScrollSpeed::ePSS_VerySlow), static_cast(ePageScrollSpeed::ePSS_Slow), static_cast(ePageScrollSpeed::ePSS_Fast), static_cast(ePageScrollSpeed::ePSS_VeryFast), - static_cast(ePageScrollSpeed::ePSS_Instant) }; - addFormSelector(F("Scroll"), F("scroll"), 5, options, optionValues, P036_SCROLL); + static_cast(ePageScrollSpeed::ePSS_Instant), +# if P036_ENABLE_TICKER + static_cast(ePageScrollSpeed::ePSS_Ticker), +# endif // if P036_ENABLE_TICKER + }; + addFormSelector(F("Scroll"), F("scroll"), optionCnt, options, optionValues, P036_SCROLL); } // FIXME TD-er: Why is this using pin3 and not pin1? And why isn't this using the normal pin selection functions? @@ -698,6 +724,7 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) (P036_ROTATE == 2), // 1 = Normal, 2 = Rotated P036_CONTRAST, P036_TIMER, + static_cast(P036_SCROLL), // Scroll speed P036_NLINES ))) { clearPluginTaskData(event->TaskIndex); @@ -896,6 +923,12 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) if (P036_DisplayIsOn) { // Display is on. + if (!P036_data->bRunning && NetworkConnected() && (P036_data->ScrollingPages.Scrolling == 0)) { + // start page updates after network has connected + P036_data->P036_DisplayPage(event); + } + else { + P036_data->HeaderContent = static_cast(get8BitFromUL(P036_FLAGS_0, P036_FLAG_HEADER)); // HeaderContent P036_data->HeaderContentAlternative = static_cast(get8BitFromUL(P036_FLAGS_0, P036_FLAG_HEADER_ALTERNATIVE)); @@ -907,6 +940,7 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) P036_data->update_display(); } } + } success = true; break; @@ -1023,15 +1057,17 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) addLog(LOG_LEVEL_INFO, F("P036_PLUGIN_WRITE ...")); # endif // PLUGIN_036_DEBUG + bool bUpdateDisplay = false; + bool bDisplayON = false; + uint8_t eventId = 0; const String command = parseString(string, 1); - const int LineNo = event->Par1; + const String subcommand = parseString(string, 2); + int LineNo = event->Par1; # if P036_SEND_EVENTS const bool sendEvents = bitRead(P036_FLAGS_0, P036_FLAG_SEND_EVENTS); // Bit 28 Send Events # endif // if P036_SEND_EVENTS if ((equals(command, F("oledframedcmd"))) && P036_data->isInitialized()) { - const String subcommand = parseString(string, 2); - if (equals(subcommand, F("display"))) { // display functions const String para1 = parseString(string, 3); @@ -1067,49 +1103,25 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) else if (equals(para1, F("low"))) { success = true; P036_data->setContrast(OLED_CONTRAST_LOW); - # if P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 0); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // if P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 0; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } else if (equals(para1, F("med"))) { success = true; P036_data->setContrast(OLED_CONTRAST_MED); - # if P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 1); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // if P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 1; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } else if (equals(para1, F("high"))) { success = true; P036_data->setContrast(OLED_CONTRAST_HIGH); - # if P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 2); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // if P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 2; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } else if (equals(para1, F("user")) && @@ -1120,17 +1132,9 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) success = true; P036_data->display->setContrast(static_cast(event->Par3), static_cast(event->Par4), static_cast(event->Par5)); - # if P036_SEND_EVENTS - - if (sendEvents) { - P036_SendEvent(event, P036_EVENT_CONTRAST, 3); - - if (!P036_DisplayIsOn) { - P036_SendEvent(event, P036_EVENT_DISPLAY, 1); - } - } - # endif // if P036_SEND_EVENTS - P036_SetDisplayOn(1); // Save the fact that the display is now ON + LineNo = 3; // is event parameter + eventId = P036_EVENT_CONTRAST; + bDisplayON = true; } } else if ((equals(subcommand, F("frame"))) && (event->Par2 >= 0) && @@ -1162,11 +1166,19 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) else if ((equals(subcommand, F("linecount"))) && (event->Par2 >= 1) && (event->Par2 <= 4)) { + # if P036_ENABLE_TICKER + + if (static_cast(P036_SCROLL) == ePageScrollSpeed::ePSS_Ticker) { + // Ticker supports only 1 line, can not be changed + success = (event->Par2 == 1); + return success; + } + # endif // if P036_ENABLE_TICKER success = true; if (P036_NLINES != event->Par2) { P036_NLINES = event->Par2; - P036_data->setNrLines(P036_NLINES); + P036_data->setNrLines(event, P036_NLINES); # if P036_SEND_EVENTS if (sendEvents && bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE)) { // Bit 29 Send Events Frame & Line @@ -1176,6 +1188,47 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } } # endif // if P036_ENABLE_LINECOUNT + else if ((equals(subcommand, F("restore"))) && + (event->Par2 >= 0) && // 0: restore all line contents + (event->Par2 <= P36_Nlines)) { + // restore content functions + success = true; + LineNo = event->Par2; + P036_data->RestoreLineContent(event->TaskIndex, + get4BitFromUL(P036_FLAGS_0, P036_FLAG_SETTINGS_VERSION), // Bit23-20 Version CustomTaskSettings + LineNo); + + if (LineNo == 0) + LineNo = 1; // after restoring all contents start with first Line + eventId = P036_EVENT_RESTORE; + bUpdateDisplay = true; + } + else if ((equals(subcommand, F("scroll"))) && + (event->Par2 >= 1)) { + // set scroll + success = true; + + switch (event->Par2) { + case 1: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_VerySlow); break; + case 2: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Slow); break; + case 3: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Fast); break; + case 4: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_VeryFast); break; + case 5: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Instant); break; +# if P036_ENABLE_TICKER + case 6: P036_SCROLL = static_cast(ePageScrollSpeed::ePSS_Ticker); break; +# endif // if P036_ENABLE_TICKER + default: + success = false; + break; + } + + if (success) { + P036_data->prepare_pagescrolling(static_cast(P036_SCROLL), P036_NLINES); + eventId = P036_EVENT_SCROLL; + LineNo = 1; // after change scroll start with first Line + bUpdateDisplay = true; + } + } # if P036_ENABLE_LEFT_ALIGN else if ((equals(subcommand, F("leftalign"))) && ((event->Par2 == 0) || @@ -1238,6 +1291,27 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) *currentLine = currentLine->substring(0, strlen - iCharToRemove); } } + eventId = P036_FLAG_EVENTS_FRAME_LINE; + bUpdateDisplay = true; + } + } + + if (success && (eventId > 0)) { + if (bDisplayON) { + # ifdef P036_SEND_EVENTS + + if (sendEvents) { + P036_SendEvent(event, eventId, LineNo); + + if (!P036_DisplayIsOn) { + P036_SendEvent(event, P036_EVENT_DISPLAY, 1); + } + } + # endif // ifdef P036_SEND_EVENTS + P036_SetDisplayOn(1); // Save the fact that the display is now ON + } + + if (bUpdateDisplay) { P036_data->MaxFramesToDisplay = 0xff; // update frame count # if P036_SEND_EVENTS @@ -1245,7 +1319,8 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) # endif // if P036_SEND_EVENTS if (!P036_DisplayIsOn && - !bitRead(P036_FLAGS_0, P036_FLAG_NODISPLAY_ONRECEIVE)) { // Bit 18 NoDisplayOnReceivedText + (!bitRead(P036_FLAGS_0, P036_FLAG_NODISPLAY_ONRECEIVE) || // Bit 18 NoDisplayOnReceivedText + (eventId == P036_EVENT_SCROLL))) { // display was OFF, turn it ON P036_data->display->displayOn(); P036_SetDisplayOn(1); // Save the fact that the display is now ON @@ -1273,23 +1348,25 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } # ifdef PLUGIN_036_DEBUG + + if (eventId == P036_FLAG_EVENTS_FRAME_LINE) { String log; if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(200)) { // estimated - log += F("[P36] Line: "); + log = F("[P36] Line: "); log += LineNo; - log += F(" NewContent:"); - log += NewContent; log += F(" Content:"); - log += P036_data->DisplayLinesV1[LineNo - 1].Content; + log += P036_data->LineContent->DisplayLinesV1[LineNo - 1].Content; log += F(" Length:"); - log += P036_data->DisplayLinesV1[LineNo - 1].Content.length(); + log += P036_data->LineContent->DisplayLinesV1[LineNo - 1].Content.length(); log += F(" Pix: "); - log += P036_data->display->getStringWidth(P036_data->DisplayLinesV1[LineNo - 1].Content); + log += P036_data->display->getStringWidth(P036_data->LineContent->DisplayLinesV1[LineNo - 1].Content); log += F(" Reserved:"); - log += P036_data->DisplayLinesV1[LineNo - 1].reserved; + log += P036_data->LineContent->DisplayLinesV1[LineNo - 1].reserved; addLogMove(LOG_LEVEL_INFO, log); + delay(5); // otherwise it is may be to fast for the serial monitor + } } # endif // PLUGIN_036_DEBUG } @@ -1323,6 +1400,8 @@ const __FlashStringHelper* P36_eventId_toString(uint8_t eventId) # if P036_ENABLE_LINECOUNT case P036_EVENT_LINECNT: return F("linecount"); # endif // if P036_ENABLE_LINECOUNT + case P036_EVENT_RESTORE: return F("restore"); + case P036_EVENT_SCROLL: return F("scroll"); } return F(""); } diff --git a/src/src/PluginStructs/P036_data_struct.cpp b/src/src/PluginStructs/P036_data_struct.cpp index 913609506a..dd20d1a781 100644 --- a/src/src/PluginStructs/P036_data_struct.cpp +++ b/src/src/PluginStructs/P036_data_struct.cpp @@ -114,33 +114,18 @@ void P036_data_struct::reset() { # ifdef P036_FONT_CALC_LOG const __FlashStringHelper * tFontSettings::FontName() const { - if (fontData == ArialMT_Plain_24) { - return F("Arial_24"); - } - -# ifndef P036_LIMIT_BUILD_SIZE - - if (fontData == Dialog_plain_18) { - return F("Dialog_18"); - } -# endif // ifndef P036_LIMIT_BUILD_SIZE - - if (fontData == ArialMT_Plain_16) { - return F("Arial_16"); - } - -# ifndef P036_LIMIT_BUILD_SIZE - - if (fontData == Dialog_plain_12) { - return F("Dialog_12"); - } -# endif // ifndef P036_LIMIT_BUILD_SIZE - - if (fontData == ArialMT_Plain_10) { - return F("Arial_10"); - } - else { - return F("Unknown font"); + switch (fontIdx) { + case 0: return F("Arial_24"); break; + # ifndef P036_LIMIT_BUILD_SIZE + case 1: return F("Dialog_18"); break; + case 2: return F("Arial_16"); break; + case 3: return F("Dialog_12"); break; + case 4: return F("Arial_10"); break; + # else // ifndef P036_LIMIT_BUILD_SIZE + case 1: return F("Arial_16"); break; + case 2: return F("Arial_10"); break; + # endif // ifndef P036_LIMIT_BUILD_SIZE + default: return F("Unknown font"); } } @@ -195,6 +180,7 @@ bool P036_data_struct::init(taskIndex_t taskIndex, bool Rotated, uint8_t Contrast, uint16_t DisplayTimer, + ePageScrollSpeed ScrollSpeed, uint8_t NrLines) { reset(); @@ -272,9 +258,11 @@ bool P036_data_struct::init(taskIndex_t taskIndex, } // prepare font and positions for page and line scrolling - prepare_pagescrolling(); + prepare_pagescrolling(ScrollSpeed, NrLines); } + bRunning = NetworkConnected(); + return isInitialized(); } @@ -295,11 +283,30 @@ void P036_data_struct::setOrientationRotated(bool rotated) { } } +void P036_data_struct::RestoreLineContent(taskIndex_t taskIndex, + uint8_t LoadVersion, + uint8_t LineNo) { + P036_LineContent *TempContent = new (std::nothrow) P036_LineContent(); + + if (TempContent != nullptr) { + TempContent->loadDisplayLines(taskIndex, LoadVersion); + + if (LineNo == 0) { + for (int i = 0; i < P36_Nlines; ++i) { + *(&LineContent->DisplayLinesV1[i].Content) = TempContent->DisplayLinesV1[i].Content; + } + } + else { + *(&LineContent->DisplayLinesV1[LineNo - 1].Content) = TempContent->DisplayLinesV1[LineNo - 1].Content; + } + delete TempContent; + } +} + # if P036_ENABLE_LINECOUNT -void P036_data_struct::setNrLines(uint8_t NrLines) { +void P036_data_struct::setNrLines(struct EventStruct *event, uint8_t NrLines) { if ((NrLines >= 1) && (NrLines <= P36_MAX_LinesPerPage)) { - ScrollingPages.linesPerFrameDef = NrLines; - prepare_pagescrolling(); // Recalculate font + prepare_pagescrolling(static_cast(P036_SCROLL), NrLines); // Recalculate font MaxFramesToDisplay = 0xFF; // Recalculate page indicator CalcMaxPageCount(); // Update max page count nextFrameToDisplay = 0; // Reset to first page @@ -543,7 +550,7 @@ int16_t P036_data_struct::GetHeaderHeight() const { } int16_t P036_data_struct::GetIndicatorTop() const { - if (bHideFooter) { + if (bHideFooter || bUseTicker) { // no footer (indicator) -> returm max. display height return getDisplaySizeSettings(disp_resolution).Height; } @@ -580,14 +587,20 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ switch (iModifyFont) { case eModifyFont::eEnlarge: + if (ScrollingPages.linesPerFrameDef > 1) { + // Font can only be enlarged if more than 1 line is displayed lFontIndex--; - if (lFontIndex < IdxForBiggestFont) { lFontIndex = IdxForBiggestFont; } - result.IdxForBiggestFontUsed = lFontIndex; + if (lFontIndex < IdxForBiggestFont) { lFontIndex = IdxForBiggestFont; } + result.IdxForBiggestFontUsed = lFontIndex; + } break; case eModifyFont::eMaximize: - lFontIndex = IdxForBiggestFont; - result.IdxForBiggestFontUsed = lFontIndex; + if (ScrollingPages.linesPerFrameDef > 1) { + // Font can only be maximized if more than 1 line is displayed + lFontIndex = IdxForBiggestFont; + result.IdxForBiggestFontUsed = lFontIndex; + } break; case eModifyFont::eReduce: lFontIndex++; @@ -621,6 +634,9 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ // just one lines per frame -> no space inbetween lSpace = 0; lTop = (MaxHeight - lHeight) / 2; + if (lHeight > MaxHeight) { + result.NextLineNo = 0xFF; // settings do not fit + } } else { if (deltaHeight >= (lLinesPerFrame - 1)) { // individual line setting fits @@ -661,6 +677,27 @@ tIndividualFontSettings P036_data_struct::CalculateIndividualFontSettings(uint8_ LineSettings[k].ypos = LineSettings[k - 1].ypos + FontSizes[LineSettings[k - 1].fontIdx].Height + lSpace; } } +# ifdef P036_CHECK_INDIVIDUAL_FONT + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log1; + + if (log1.reserve(140)) { // estimated + delay(10); // otherwise it is may be to fast for the serial monitor + log1 = F("IndividualFontSettings:"); + log1 += F(" result.NextLineNo:"); log1 += result.NextLineNo; + log1 += F(" result.IdxForBiggestFontUsed:"); log1 += result.IdxForBiggestFontUsed; + log1 += F(" LineNo:"); log1 += LineNo; + log1 += F(" LinesPerFrame:"); log1 += LinesPerFrame; + if (result.NextLineNo != 0xFF) { + log1 += F(" FrameNo:"); log1 += FrameNo; + log1 += F(" lTop:"); log1 += lTop; + log1 += F(" lSpace:"); log1 += lSpace; + } + addLogMove(LOG_LEVEL_INFO, log1); + } + } +#endif // # ifdef P036_CHECK_INDIVIDUAL_FONT return result; } @@ -690,10 +727,9 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { strformat(F("P036 CalculateFontSettings lines: %d, height: %d, header: %s, footer: %s"), iLinesPerFrame, iHeight, - boolToString(!bHideHeader).c_str(), - boolToString(!bHideFooter).c_str())); + boolToString(!bHideHeader), + boolToString(!bHideFooter))); } - String log; # endif // ifdef P036_FONT_CALC_LOG iMaxHeightForFont = lround(iHeight / (iLinesPerFrame * 1.0f)); // no extra space between lines @@ -712,13 +748,13 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { # ifdef P036_FONT_CALC_LOG String log1; log1.reserve(80); - log1.clear(); # endif // ifdef P036_FONT_CALC_LOG for (i = 0; i < P36_MaxFontCount - 1; i++) { // check available fonts for the line setting # ifdef P036_FONT_CALC_LOG - log1 += F(" -> i: "); + delay(5); // otherwise it is may be to fast for the serial monitor + log1 = F(" -> i: "); log1 += i; log1 += F(", h: "); log1 += FontSizes[i].Height; @@ -740,7 +776,7 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { # ifdef P036_FONT_CALC_LOG log1 += F(", no font fits, fontIdx: "); log1 += iFontIndex; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); # endif // ifdef P036_FONT_CALC_LOG break; @@ -793,6 +829,13 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { uint8_t iIdxForBiggestFont = 0; while (currentLine < P36_Nlines) { +# if P036_ENABLE_TICKER + + if (bUseTicker && (currentLine > 0)) { + // for ticker only the first line defines the font + break; + } +# endif // if P036_ENABLE_TICKER // calculate individual font settings IndividualFontSettings = CalculateIndividualFontSettings(currentLine, iFontIndex, @@ -803,8 +846,8 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { if (IndividualFontSettings.NextLineNo == 0xFF) { // individual settings do not fit - if (bReduceLinesPerFrame) { - currentLinesPerFrame--; // reduce numer of lines per frame + if ((bReduceLinesPerFrame) && (currentLinesPerFrame > 1)) { + currentLinesPerFrame--; // reduce number of lines per frame } else { iIdxForBiggestFont = IndividualFontSettings.IdxForBiggestFontUsed + 1; // use smaller font size as maximum } @@ -823,24 +866,15 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { String log1; if (log1.reserve(140)) { // estimated - log1.clear(); - log1 = F("IndividualFontSettings:"); - log1 += F(" iFontIndex:"); log1 += iFontIndex; - log1 += F(" iLinesPerFrame:"); log1 += iLinesPerFrame; - log1 += F(" TopLineOffset:"); log1 += TopLineOffset; - log1 += F(" iHeight:"); log1 += iHeight; - log1 += F(" iUsedHeightForFonts:"); log1 += iUsedHeightForFonts; - log1 += F(" iMaxHeightForFont:"); log1 += iMaxHeightForFont; - addLog(LOG_LEVEL_INFO, log1); - for (uint8_t i = 0; i < P36_Nlines; i++) { + delay(5); // otherwise it is may be to fast for the serial monitor log1.clear(); - log1 += F("Line["); log1 += i; + log1 = F("Line["); log1 += i; log1 += F("]: Frame:"); log1 += LineSettings[i].frame; log1 += F(" FontIdx:"); log1 += LineSettings[i].fontIdx; log1 += F(" ypos:"); log1 += LineSettings[i].ypos - TopLineOffset; log1 += F(" FontHeight:"); log1 += LineSettings[i].FontHeight; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); } } } @@ -855,7 +889,7 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { String log1; if (log1.reserve(140)) { // estimated - log1.clear(); + delay(5); // otherwise it is may be to fast for the serial monitor log1 = F("CalculateFontSettings: Font:"); log1 += result.FontName(); log1 += F(" Idx:"); @@ -887,10 +921,23 @@ tFontSettings P036_data_struct::CalculateFontSettings(uint8_t lDefaultLines) { return result; } -void P036_data_struct::prepare_pagescrolling() { +void P036_data_struct::prepare_pagescrolling(ePageScrollSpeed lscrollspeed, + uint8_t NrLines) { if (!isInitialized()) { return; } +# if P036_ENABLE_TICKER + bUseTicker = (lscrollspeed == ePageScrollSpeed::ePSS_Ticker); +# else // if P036_ENABLE_TICKER + bUseTicker = false; +# endif //if P036_ENABLE_TICKER + + if (bUseTicker) { + ScrollingPages.linesPerFrameDef = 1; + } + else { + ScrollingPages.linesPerFrameDef = NrLines; + } CalculateFontSettings(0); } @@ -908,7 +955,7 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(32)) { - log += F("Start Scrolling: Speed: "); + log = F("Start Scrolling: Speed: "); log += static_cast(lscrollspeed); addLogMove(LOG_LEVEL_INFO, log); } @@ -920,6 +967,9 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (lscrollspeed == ePageScrollSpeed::ePSS_Instant) { // no scrolling, just the handling time to build the new page iPageScrollTime = P36_PageScrollTick - P36_PageScrollTimer; + } else if (lscrollspeed == ePageScrollSpeed::ePSS_Ticker) { + // for ticker, no scrolling, just the handling time to build the new page + iPageScrollTime = P36_PageScrollTick - P36_PageScrollTimer; } else { iPageScrollTime = (P36_MaxDisplayWidth / (P36_PageScrollPix * static_cast(lscrollspeed))) * P36_PageScrollTick; } @@ -930,7 +980,7 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log; log.reserve(32); - log += F("PageScrollTime: "); + log = F("PageScrollTime: "); log += iPageScrollTime; addLogMove(LOG_LEVEL_INFO, log); } @@ -943,6 +993,22 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas MaxPixWidthForPageScrolling -= getDisplaySizeSettings(disp_resolution).PixLeft; } +# if P036_ENABLE_TICKER + + if (bUseTicker) { + ScrollingLines.Ticker.Tcontent = EMPTY_STRING; + ScrollingLines.Ticker.IdxEnd = 0; + ScrollingLines.Ticker.IdxStart = 0; + + for (uint8_t i = 0; i < P36_Nlines; i++) { + String tmpString(LineContent->DisplayLinesV1[i].Content); + tmpString.replace(F("<|>"), " "); // replace the split token with three space char + ScrollingLines.Ticker.Tcontent += P36_parseTemplate(tmpString, i); + } + ScrollingLines.Ticker.len = ScrollingLines.Ticker.Tcontent.length(); + } +# endif // if P036_ENABLE_TICKER + for (uint8_t j = 0; j < ScrollingPages.linesPerFrameDef; j++) { // default no line scrolling and strings are centered uint16_t PixLengthLineOut = 0; // pix length of line out @@ -965,9 +1031,56 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas ScrollingLines.SLine[j].LastWidth = PixLengthLineOut; // while page scrolling this line is right aligned } - if ((PixLengthLineIn > getDisplaySizeSettings(disp_resolution).Width) && + if ((bUseTicker || (PixLengthLineIn > getDisplaySizeSettings(disp_resolution).Width)) && (iScrollTime > 0)) { // width of the line > display width -> scroll line + if (bUseTicker) { +# if P036_ENABLE_TICKER + ScrollingLines.SLine[j].Width = 0; + uint16_t AddPixTicker; + + switch (textAlignment) { + case TEXT_ALIGN_CENTER: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft + + getDisplaySizeSettings(disp_resolution).Width / 2; + AddPixTicker = getDisplaySizeSettings(disp_resolution).Width / 2; // half width at begin + break; + case TEXT_ALIGN_RIGHT: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft + + getDisplaySizeSettings(disp_resolution).Width; + AddPixTicker = getDisplaySizeSettings(disp_resolution).Width; // full width at begin + break; + default: ScrollingLines.SLine[j].CurrentLeft = getDisplaySizeSettings(disp_resolution).PixLeft; + AddPixTicker = 0; + } + ScrollingLines.SLine[j].fPixSum = ScrollingLines.SLine[j].CurrentLeft; + + display->setFont(FontSizes[LineSettings[j].fontIdx].fontData); + ScrollingLines.SLine[j].dPix = (static_cast(display->getStringWidth(ScrollingLines.Ticker.Tcontent) + AddPixTicker)) / + static_cast(iScrollTime); + ScrollingLines.SLine[j].SLcontent = EMPTY_STRING; + + ScrollingLines.Ticker.TickerAvgPixPerChar = lround(static_cast(display->getStringWidth( + ScrollingLines.Ticker.Tcontent)) / + static_cast(ScrollingLines.Ticker.len)); + + if (ScrollingLines.Ticker.TickerAvgPixPerChar < ScrollingLines.SLine[j].dPix) { + ScrollingLines.Ticker.TickerAvgPixPerChar = round(2 * ScrollingLines.SLine[j].dPix); + } + ScrollingLines.Ticker.MaxPixLen = getDisplaySizeSettings(disp_resolution).Width + 2 * ScrollingLines.Ticker.TickerAvgPixPerChar; + + // add more characters to display + while (true) { + char c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxEnd); + uint8_t PixForChar = display->getCharWidth(c); + + if ((ScrollingLines.SLine[0].Width + PixForChar) >= ScrollingLines.Ticker.MaxPixLen) { + break; // no more characters necessary to add + } + ScrollingLines.Ticker.IdxEnd++; + ScrollingLines.SLine[j].Width += PixForChar; + } +# endif // if P036_ENABLE_TICKER + } + else { ScrollingLines.SLine[j].SLcontent = ScrollingPages.In[j].SPLcontent; ScrollingLines.SLine[j].SLidx = ScrollingPages.In[j].SPLidx; // index to LineSettings[] ScrollingLines.SLine[j].Width = PixLengthLineIn; // while page scrolling this line is left aligned @@ -977,19 +1090,38 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas // pix change per scrolling line tick ScrollingLines.SLine[j].dPix = (static_cast(PixLengthLineIn - getDisplaySizeSettings(disp_resolution).Width)) / iScrollTime; + } # ifdef P036_SCROLL_CALC_LOG if (loglevelActiveFor(LOG_LEVEL_INFO)) { + delay(5); // otherwise it is may be to fast for the serial monitor String log; log.reserve(32); - log += F("Line: "); + log = F("Line: "); log += (j + 1); log += F(" width: "); log += ScrollingLines.SLine[j].Width; log += F(" dPix: "); log += ScrollingLines.SLine[j].dPix; addLogMove(LOG_LEVEL_INFO, log); +# if P036_ENABLE_TICKER + + if (bUseTicker) { + delay(5); // otherwise it is may be to fast for the serial monitor + String log1; + log1.reserve(200); + log1 = F("+++ iScrollTime: "); + log1 += iScrollTime; + log1 += F(" StrLength: "); + log1 += ScrollingLines.Ticker.len; + log1 += F(" StrInPix: "); + log1 += display->getStringWidth(ScrollingLines.Ticker.Tcontent); + log1 += F(" PixPerChar: "); + log1 += ScrollingLines.Ticker.TickerAvgPixPerChar; + addLogMove(LOG_LEVEL_INFO, log1); + } +# endif // if P036_ENABLE_TICKER } # endif // P036_SCROLL_CALC_LOG } @@ -1040,13 +1172,14 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(128)) { - log += F("Line: "); log += (j + 1); + delay(5); // otherwise it is may be to fast for the serial monitor + log = F("Line: "); log += (j + 1); log += F(" LineIn: "); log += LineInStr; log += F(" Length: "); log += strlen; log += F(" PixLength: "); log += PixLengthLineIn; log += F(" AvgPixPerChar: "); log += fAvgPixPerChar; log += F(" CharsRemoved: "); log += iCharToRemove; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); log.clear(); log += F(" -> Changed to: "); log += ScrollingPages.In[j].SPLcontent; log += F(" Length: "); log += ScrollingPages.In[j].SPLcontent.length(); @@ -1145,13 +1278,15 @@ uint8_t P036_data_struct::display_scroll(ePageScrollSpeed lscrollspeed, int lTas if (loglevelActiveFor(LOG_LEVEL_INFO) && log.reserve(128)) { - log += F("Line: "); log += (j + 1); + delay(5); // otherwise it is may be to fast for the serial monitor + log = F("Line: "); log += (j + 1); log += F(" LineOut: "); log += LineOutStr; log += F(" Length: "); log += strlen; log += F(" PixLength: "); log += PixLengthLineOut; log += F(" AvgPixPerChar: "); log += fAvgPixPerChar; log += F(" CharsRemoved: "); log += iCharToRemove; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); + delay(5); // otherwise it is may be to fast for the serial monitor log.clear(); log += F(" -> Changed to: "); log += ScrollingPages.Out[j].SPLcontent; log += F(" Length: "); log += ScrollingPages.Out[j].SPLcontent.length(); @@ -1198,18 +1333,30 @@ uint8_t P036_data_struct::display_scroll_timer(bool initialScroll, } } + if (!bUseTicker) { for (uint8_t j = 0; j < ScrollingPages.linesPerFrameOut; j++) { if ((initialScroll && (lscrollspeed < ePageScrollSpeed::ePSS_Instant)) || !initialScroll) { - // scrolling, prepare scrolling out to right + // scrolling, prepare scrolling page out to right DrawScrollingPageLine(&ScrollingPages.Out[j], ScrollingLines.SLine[j].LastWidth, TEXT_ALIGN_RIGHT); } } for (uint8_t j = 0; j < ScrollingPages.linesPerFrameIn; j++) { - // non-scrolling or scrolling prepare scrolling in from left + // non-scrolling or scrolling prepare scrolling page in from left DrawScrollingPageLine(&ScrollingPages.In[j], ScrollingLines.SLine[j].Width, TEXT_ALIGN_LEFT); } + } +# if P036_ENABLE_TICKER + else { + // for Ticker start with the set alignment + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[0].SLidx].fontIdx].fontData); + display->drawString(ScrollingLines.SLine[0].CurrentLeft, + LineSettings[ScrollingLines.SLine[0].SLidx].ypos, + ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd)); + } +# endif // if P036_ENABLE_TICKER update_display(); @@ -1245,9 +1392,8 @@ void P036_data_struct::display_scrolling_lines() { } if (bscroll) { - ScrollingLines.wait++; - if (ScrollingLines.wait < P36_WaitScrollLines) { + ScrollingLines.wait++; return; // wait before scrolling line not finished } @@ -1268,18 +1414,74 @@ void P036_data_struct::display_scrolling_lines() { display->setFont(FontSizes[LineSettings[ScrollingLines.SLine[i].SLidx].fontIdx].fontData); - if (((ScrollingLines.SLine[i].CurrentLeft - getDisplaySizeSettings(disp_resolution).PixLeft) + - ScrollingLines.SLine[i].Width) >= getDisplaySizeSettings(disp_resolution).Width) { + if (bUseTicker || (((iCurrentLeft - getDisplaySizeSettings(disp_resolution).PixLeft) + + ScrollingLines.SLine[i].Width) >= getDisplaySizeSettings(disp_resolution).Width)) { display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(ScrollingLines.SLine[i].CurrentLeft, + + if (bUseTicker) { +#if P036_ENABLE_TICKER + display->drawString(iCurrentLeft, + LineSettings[ScrollingLines.SLine[0].SLidx].ypos, + ScrollingLines.Ticker.Tcontent.substring(ScrollingLines.Ticker.IdxStart, ScrollingLines.Ticker.IdxEnd)); + + // add more characters to display + iCurrentLeft -= getDisplaySizeSettings(disp_resolution).PixLeft; + + while (true) { + if (ScrollingLines.Ticker.IdxEnd >= ScrollingLines.Ticker.len) {// end of string + break; + } + uint8_t c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxEnd); + uint8_t PixForChar = display->getCharWidth(c);// PixForChar can be 0 if c is non ascii + + if ((static_cast(ScrollingLines.SLine[0].Width + PixForChar) + iCurrentLeft) >= ScrollingLines.Ticker.MaxPixLen) { + break; // no more characters necessary to add + } + ScrollingLines.Ticker.IdxEnd++; + ScrollingLines.SLine[0].Width += PixForChar; + } + + // remove already displayed characters + float fCurrentPixLeft = static_cast(getDisplaySizeSettings(disp_resolution).PixLeft) - 2.0f * + ScrollingLines.Ticker.TickerAvgPixPerChar; + + while (ScrollingLines.SLine[0].fPixSum < fCurrentPixLeft) { + uint8_t c = ScrollingLines.Ticker.Tcontent.charAt(ScrollingLines.Ticker.IdxStart); + uint8_t PixForChar = display->getCharWidth(c);// PixForChar can be 0 if c is non ascii + ScrollingLines.SLine[0].fPixSum += static_cast(PixForChar); + ScrollingLines.Ticker.IdxStart++; + + if (ScrollingLines.Ticker.IdxStart >= ScrollingLines.Ticker.IdxEnd) { + ScrollingLines.SLine[0].Width = 0;// Stop scrolling + +# ifdef PLUGIN_036_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, F("Ticker finished")); + } +# endif // PLUGIN_036_DEBUG + break; + } + + if (ScrollingLines.SLine[0].Width > PixForChar) { + ScrollingLines.SLine[0].Width -= PixForChar; + } + } + break; +#endif // if P036_ENABLE_TICKER + } else { + display->drawString(iCurrentLeft, LineSettings[ScrollingLines.SLine[i].SLidx].ypos, ScrollingLines.SLine[i].SLcontent); + } } else { + if (!bUseTicker) { // line scrolling finished -> line is shown as aligned right display->setTextAlignment(TEXT_ALIGN_RIGHT); display->drawString(P36_MaxDisplayWidth - getDisplaySizeSettings(disp_resolution).PixLeft, LineSettings[ScrollingLines.SLine[i].SLidx].ypos, ScrollingLines.SLine[i].SLcontent); + } ScrollingLines.SLine[i].Width = 0; // Stop scrolling } } @@ -1369,7 +1571,7 @@ void P036_data_struct::P036_JumpToPage(struct EventStruct *event, uint8_t nextFr bPageScrollDisabled = true; // show next page without scrolling disableFrameChangeCnt = 2; // disable next page change in PLUGIN_READ if PLUGIN_READ was already scheduled P036_DisplayPage(event); // Display the selected page, function needs 65ms! - displayTimer = PCONFIG(4); // Restart timer + displayTimer = P036_TIMER; // Restart timer } void P036_data_struct::P036_JumpToPageOfLine(struct EventStruct *event, uint8_t LineNo) @@ -1378,6 +1580,9 @@ void P036_data_struct::P036_JumpToPageOfLine(struct EventStruct *event, uint8_t P036_JumpToPage(event, LineSettings[LineNo].DisplayedPageNo); } +// Defines the Scroll area layout +// Displays the selected page, function needs 65ms! +// Called by PLUGIN_READ and P036_JumpToPage() void P036_data_struct::P036_DisplayPage(struct EventStruct *event) { # ifdef PLUGIN_036_DEBUG @@ -1478,26 +1683,28 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) # if P036_FEATURE_DISPLAY_PREVIEW && P036_FEATURE_ALIGN_PREVIEW - // Preview: Center or Right-Align add spaces on the left - const bool isAlignCenter = ScrollingPages.In[i].Alignment == OLEDDISPLAY_TEXT_ALIGNMENT::TEXT_ALIGN_CENTER; - const bool isAlignRight = ScrollingPages.In[i].Alignment == OLEDDISPLAY_TEXT_ALIGNMENT::TEXT_ALIGN_RIGHT; + if (!bUseTicker) { + // Preview: Center or Right-Align add spaces on the left + const bool isAlignCenter = ScrollingPages.In[i].Alignment == OLEDDISPLAY_TEXT_ALIGNMENT::TEXT_ALIGN_CENTER; + const bool isAlignRight = ScrollingPages.In[i].Alignment == OLEDDISPLAY_TEXT_ALIGNMENT::TEXT_ALIGN_RIGHT; - if (isAlignRight || isAlignCenter) { - const uint16_t maxlength = getDisplaySizeSettings(disp_resolution).Width; - const uint16_t pixlength = display->getStringWidth(currentLines[i]); // pix length for entire string - const uint16_t charlength = display->getStringWidth(F(" ")); // pix length for a space char - int16_t addSpaces = (maxlength - pixlength) / charlength; + if (isAlignRight || isAlignCenter) { + const uint16_t maxlength = getDisplaySizeSettings(disp_resolution).Width; + const uint16_t pixlength = display->getStringWidth(currentLines[i]); // pix length for entire string + const uint16_t charlength = display->getStringWidth(F(" ")); // pix length for a space char + int16_t addSpaces = (maxlength - pixlength) / charlength; - if (isAlignCenter) { - addSpaces /= 2; - } + if (isAlignCenter) { + addSpaces /= 2; + } - if (addSpaces > 0) { - currentLines[i].reserve(currentLines[i].length() + addSpaces); + if (addSpaces > 0) { + currentLines[i].reserve(currentLines[i].length() + addSpaces); - while (addSpaces > 0) { - currentLines[i] = ' ' + currentLines[i]; - addSpaces--; + while (addSpaces > 0) { + currentLines[i] = ' ' + currentLines[i]; + addSpaces--; + } } } } @@ -1537,10 +1744,12 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) const bool bScrollWithoutWifi = bitRead(PCONFIG_LONG(0), 24); // Bit 24 const bool bScrollLines = bitRead(PCONFIG_LONG(0), 17); // Bit 17 - bLineScrollEnabled = (bScrollLines && (NetworkConnected() || bScrollWithoutWifi)); // scroll lines only if WifiIsConnected, + bRunning = NetworkConnected() || bScrollWithoutWifi; + bLineScrollEnabled = ((bScrollLines || bUseTicker) && bRunning);// scroll lines only if WifiIsConnected, + // WifiIsConnected, // otherwise too slow - ePageScrollSpeed lscrollspeed = static_cast(PCONFIG(3)); + ePageScrollSpeed lscrollspeed = static_cast(P036_SCROLL); if (bPageScrollDisabled) { lscrollspeed = ePageScrollSpeed::ePSS_Instant; } // first page after INIT without scrolling @@ -1550,7 +1759,7 @@ void P036_data_struct::P036_DisplayPage(struct EventStruct *event) Scheduler.setPluginTaskTimer(P36_PageScrollTimer, event->TaskIndex, event->Par1); // calls next page scrollng tick } - if (NetworkConnected() || bScrollWithoutWifi) { + if (bRunning) { // scroll lines only if WifiIsConnected, otherwise too slow bPageScrollDisabled = false; // next PLUGIN_READ will do page scrolling } @@ -1582,7 +1791,16 @@ String P036_data_struct::P36_parseTemplate(String& tmpString, uint8_t lineIdx) { const eAlignment iAlignment = static_cast(get3BitFromUL(LineContent->DisplayLinesV1[lineIdx].ModifyLayout, P036_FLAG_ModifyLayout_Alignment)); - switch (getTextAlignment(iAlignment)) { + OLEDDISPLAY_TEXT_ALIGNMENT iTextAlignment = getTextAlignment(static_cast(iAlignment)); + +# if P036_ENABLE_TICKER + + if (bUseTicker) { + iTextAlignment = TEXT_ALIGN_RIGHT; // ticker is always right aligned + } +# endif // if P036_ENABLE_TICKER + + switch (iTextAlignment) { case TEXT_ALIGN_LEFT: // add leading spaces from tmpString to the result @@ -1727,19 +1945,19 @@ void P036_data_struct::CalcMaxPageCount(void) { String log1; if (log1.reserve(140)) { // estimated - log1.clear(); log1 = F("CalcMaxPageCount: MaxFramesToDisplay:"); log1 += MaxFramesToDisplay; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); for (uint8_t i = 0; i < P36_Nlines; i++) { log1.clear(); - log1 += F("Line["); log1 += i; + delay(5); // otherwise it is may be to fast for the serial monitor + log1 = F("Line["); log1 += i; log1 += F("]: Frame:"); log1 += LineSettings[i].frame; log1 += F(" DisplayedPageNo:"); log1 += LineSettings[i].DisplayedPageNo; log1 += F(" FontIdx:"); log1 += LineSettings[i].fontIdx; log1 += F(" ypos:"); log1 += LineSettings[i].ypos - TopLineOffset; log1 += F(" FontHeight:"); log1 += LineSettings[i].FontHeight; - addLog(LOG_LEVEL_INFO, log1); + addLogMove(LOG_LEVEL_INFO, log1); } } } @@ -1771,6 +1989,15 @@ void P036_data_struct::DrawScrollingPageLine(tScrollingPageLines *Scrollin OLEDDISPLAY_TEXT_ALIGNMENT textAlignment) { int16_t LeftOffset = 0; + switch (textAlignment) { + case TEXT_ALIGN_LEFT: LeftOffset = -P36_MaxDisplayWidth; + break; + case TEXT_ALIGN_RIGHT: LeftOffset = 0; + break; + default: + LeftOffset = 0; + break; + } display->setFont(FontSizes[LineSettings[ScrollingPageLine->SPLidx].fontIdx].fontData); if (Width > 0) { @@ -1784,10 +2011,6 @@ void P036_data_struct::DrawScrollingPageLine(tScrollingPageLines *Scrollin // line is kept aligned while scrolling page display->setTextAlignment(ScrollingPageLine->Alignment); - if (textAlignment == TEXT_ALIGN_LEFT) { - LeftOffset = -P36_MaxDisplayWidth; - } - // textAlignment=TEXT_ALIGN_LEFT: for non-scrolling pages ScrollingPages.dPixSum=P36_MaxDisplayWidth -> therefore the calculation must // use P36_MaxDisplayWidth, too display->drawString(LeftOffset + GetTextLeftMargin(ScrollingPageLine->Alignment) + ScrollingPages.dPixSum, @@ -1797,6 +2020,12 @@ void P036_data_struct::DrawScrollingPageLine(tScrollingPageLines *Scrollin } void P036_data_struct::CreateScrollingPageLine(tScrollingPageLines *ScrollingPageLine, uint8_t Counter) { + if (bUseTicker) { +# if P036_ENABLE_TICKER + ScrollingPageLine->SPLcontent = EMPTY_STRING; +# endif // if P036_ENABLE_TICKER + } + else { String tmpString(LineContent->DisplayLinesV1[Counter].Content); ScrollingPageLine->SPLcontent = P36_parseTemplate(tmpString, Counter); @@ -1828,6 +2057,7 @@ void P036_data_struct::CreateScrollingPageLine(tScrollingPageLines *ScrollingPag ScrollingPageLine->Alignment = getTextAlignment(iAlignment); ScrollingPageLine->SPLidx = Counter; // index to LineSettings[] } +} # if P036_FEATURE_DISPLAY_PREVIEW bool P036_data_struct::web_show_values() { diff --git a/src/src/PluginStructs/P036_data_struct.h b/src/src/PluginStructs/P036_data_struct.h index 6aaeaf2b2b..e36ddf866f 100644 --- a/src/src/PluginStructs/P036_data_struct.h +++ b/src/src/PluginStructs/P036_data_struct.h @@ -26,7 +26,7 @@ # ifndef P036_FEATURE_DISPLAY_PREVIEW # define P036_FEATURE_DISPLAY_PREVIEW 1 # endif // ifndef P036_FEATURE_DISPLAY_PREVIEW -# ifdef P036_FEATURE_ALIGN_PREVIEW +# ifndef P036_FEATURE_ALIGN_PREVIEW # define P036_FEATURE_ALIGN_PREVIEW 1 # endif // ifdef P036_FEATURE_ALIGN_PREVIEW @@ -45,6 +45,9 @@ # ifndef P036_USERDEF_HEADERS # define P036_USERDEF_HEADERS 1 // Enable User defined headers # endif // ifndef P036_USERDEF_HEADERS +# ifndef P036_ENABLE_TICKER +# define P036_ENABLE_TICKER 1 // Enable ticker function +# endif // ifndef # else // ifndef P036_LIMIT_BUILD_SIZE # if defined(P036_SEND_EVENTS) && P036_SEND_EVENTS # undef P036_SEND_EVENTS @@ -65,6 +68,9 @@ // # ifndef P036_USERDEF_HEADERS // # define P036_USERDEF_HEADERS 0 // Disable User defined headers // # endif // ifndef P036_USERDEF_HEADERS +# ifndef P036_ENABLE_TICKER +# define P036_ENABLE_TICKER 0 // Disable ticker function +# endif // ifndef # endif // ifndef P036_LIMIT_BUILD_SIZE # ifndef P036_USERDEF_HEADERS # define P036_USERDEF_HEADERS 1 // Enable User defined headers if not handled yet @@ -128,7 +134,7 @@ # define P036_FLAG_SCROLL_WITHOUTWIFI 24 // Bit 24 ScrollWithoutWifi # define P036_FLAG_HIDE_HEADER 25 // Bit 25 Hide header # define P036_FLAG_INPUT_PULLUP 26 // Bit 26 Input PullUp -// # define P036_FLAG_INPUT_PULLDOWN 27 // Bit 27 Input PullDown, 2022-09-04 no longer used +// # define P036_FLAG_INPUT_PULLDOWN 27 // Bit 27 Input PullDown, 2022-09-04 not longer used # define P036_FLAG_SEND_EVENTS 28 // Bit 28 SendEvents # define P036_FLAG_EVENTS_FRAME_LINE 29 // Bit 29 SendEvents also on Frame & Line # define P036_FLAG_HIDE_FOOTER 30 // Bit 30 Hide footer @@ -169,7 +175,8 @@ enum class ePageScrollSpeed : uint8_t { ePSS_Slow = 2u, // 400ms ePSS_Fast = 4u, // 200ms ePSS_VeryFast = 8u, // 100ms - ePSS_Instant = 32u // 20ms + ePSS_Instant = 32u, // 20ms + ePSS_Ticker = 255u // tickerspeed depends on line length }; enum class eP036pinmode : uint8_t { @@ -187,8 +194,20 @@ typedef struct { uint8_t SLidx = 0; // index to DisplayLinesV1 } tScrollLine; +typedef struct { + String Tcontent; // content (all parsed lines) + uint16_t len = 0; // length of content + uint16_t IdxStart = 0; // Start index of TickerContent for displaying (left side) + uint16_t IdxEnd = 0; // End index of TickerContent for displaying (right side) + uint16_t TickerAvgPixPerChar = 0; // max of average pixel per character or pix change per scroll time (100ms) + int16_t MaxPixLen = 0; // Max pix length to display (display width + 2*TickerAvgPixPerChar) +} tTicker; + typedef struct { tScrollLine SLine[P36_MAX_LinesPerPage]{}; +# if P036_ENABLE_TICKER + tTicker Ticker; +# endif // if P036_ENABLE_TICKER uint16_t wait = 0; // waiting time before scrolling } tScrollingLines; @@ -203,7 +222,7 @@ typedef struct { tScrollingPageLines Out[P36_MAX_LinesPerPage]{}; int dPixSum = 0; // act pix change uint8_t Scrolling = 0; // 0=Ready, 1=Scrolling - uint8_t dPix = 0; // pix change per scroll time (25ms) + uint8_t dPix = 0; // pix change per scroll time (25ms per page, 100ms per line) uint8_t linesPerFrameDef = 0; // the default number of lines in frame in/out uint8_t linesPerFrameIn = 0; // the number of lines in frame in uint8_t linesPerFrameOut = 0; // the number of lines in frame out @@ -343,6 +362,7 @@ struct P036_data_struct : public PluginTaskData_base { bool Rotated, uint8_t Contrast, uint16_t DisplayTimer, + ePageScrollSpeed ScrollSpeed, uint8_t NrLines); bool isInitialized() const; @@ -355,9 +375,16 @@ struct P036_data_struct : public PluginTaskData_base { void setOrientationRotated(bool rotated); # if P036_ENABLE_LINECOUNT - void setNrLines(uint8_t NrLines); + void setNrLines(struct EventStruct *event, + uint8_t NrLines); # endif // if P036_ENABLE_LINECOUNT + // Restores line content from flash memory + // LineNo == 0: all line contents + // otherwise just the line content of the given LineNo + void RestoreLineContent(taskIndex_t taskIndex, + uint8_t LoadVersion, + uint8_t LineNo); // The screen is set up as: // - 10 rows at the top for the header @@ -368,7 +395,8 @@ struct P036_data_struct : public PluginTaskData_base { void display_title(const String& title); void display_logo(); void display_indicator(); - void prepare_pagescrolling(); + void prepare_pagescrolling(ePageScrollSpeed lscrollspeed, + uint8_t NrLines); uint8_t display_scroll(ePageScrollSpeed lscrollspeed, int lTaskTimer); uint8_t display_scroll_timer(bool initialScroll = false, @@ -417,8 +445,8 @@ struct P036_data_struct : public PluginTaskData_base { // Instantiate display here - does not work to do this within the INIT call OLEDDisplay *display = nullptr; - tScrollingLines ScrollingLines{}; - tScrollingPages ScrollingPages{}; + tScrollingLines ScrollingLines{}; // scrolling lines in from right, out to left + tScrollingPages ScrollingPages{}; // scrolling pages in from left, out to right // CustomTaskSettings P036_LineContent *LineContent = nullptr; @@ -453,6 +481,8 @@ struct P036_data_struct : public PluginTaskData_base { uint8_t frameCounter = 0; // need to keep track of framecounter from call to call uint8_t disableFrameChangeCnt = 0; // counter to disable frame change after JumpToPage in case PLUGIN_READ already scheduled bool bPageScrollDisabled = true; // first page after INIT or after JumpToPage without scrolling + bool bRunning = false; // page updates are rumming = (NetworkConnected() || bScrollWithoutWifi) + bool bUseTicker = false; // scroll line like a ticker OLEDDISPLAY_TEXT_ALIGNMENT textAlignment = TEXT_ALIGN_CENTER; From 8885c078227bd53aea24bfbe661ced25720a946c Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Sun, 27 Aug 2023 16:48:15 +0200 Subject: [PATCH 7/9] Wrong #ifdef P036_SEND_EVENTS --- src/_P036_FrameOLED.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index 7c16d3fe50..58949ca50c 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -1298,7 +1298,7 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) if (success && (eventId > 0)) { if (bDisplayON) { - # ifdef P036_SEND_EVENTS + # if P036_SEND_EVENTS if (sendEvents) { P036_SendEvent(event, eventId, LineNo); @@ -1307,7 +1307,7 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) P036_SendEvent(event, P036_EVENT_DISPLAY, 1); } } - # endif // ifdef P036_SEND_EVENTS + # endif // if P036_SEND_EVENTS P036_SetDisplayOn(1); // Save the fact that the display is now ON } From 964a35797a18f75ccc9d6db119c17814ce7ed5b7 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Mon, 28 Aug 2023 18:43:17 +0200 Subject: [PATCH 8/9] CHG: Disable scrolling or ticker if new line content received (PLUGIN_WRITE) - Sometimes exception if the content was updated by an IR command (P016), but sending the command by Tools->Command->Submit never resulted in an exception! - Wrong eventId if new line content received - Typo in comment --- src/_P036_FrameOLED.ino | 39 +++++++++++++----------- src/src/PluginStructs/P036_data_struct.h | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index 58949ca50c..cd6465c2a6 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -1272,26 +1272,28 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) *currentLine = parseStringKeepCaseNoTrim(string, 3); *currentLine = P036_data->P36_parseTemplate(*currentLine, LineNo - 1); - // calculate Pix length of new Content - uint16_t PixLength = P036_data->CalcPixLength(LineNo - 1); + if (!P036_data->bUseTicker) { + // calculate Pix length of new content, not necessary for ticker + uint16_t PixLength = P036_data->CalcPixLength(LineNo - 1); - if (PixLength > 255) { - String str_error = F("Pixel length of "); - str_error += PixLength; - str_error += F(" too long for line! Max. 255 pix!"); - addHtmlError(str_error); + if (PixLength > 255) { + String str_error = F("Pixel length of "); + str_error += PixLength; + str_error += F(" too long for line! Max. 255 pix!"); + addHtmlError(str_error); - const unsigned int strlen = currentLine->length(); + const unsigned int strlen = currentLine->length(); - if (strlen > 0) { - const float fAvgPixPerChar = static_cast(PixLength) / strlen; - const unsigned int iCharToRemove = ceilf((static_cast(PixLength - 255)) / fAvgPixPerChar); + if (strlen > 0) { + const float fAvgPixPerChar = static_cast(PixLength) / strlen; + const unsigned int iCharToRemove = ceilf((static_cast(PixLength - 255)) / fAvgPixPerChar); - // shorten string because OLED controller can not handle such long strings - *currentLine = currentLine->substring(0, strlen - iCharToRemove); + // shorten string because OLED controller can not handle such long strings + *currentLine = currentLine->substring(0, strlen - iCharToRemove); + } } } - eventId = P036_FLAG_EVENTS_FRAME_LINE; + eventId = P036_EVENT_LINE; bUpdateDisplay = true; } } @@ -1337,8 +1339,11 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) } if (P036_DisplayIsOn) { - P036_data->P036_JumpToPageOfLine(event, LineNo - 1); // Start to display the selected page - // function needs 65ms! + P036_data->bLineScrollEnabled=false; // disable scrolling temporary + if (P036_data->bUseTicker) + P036_data->P036_JumpToPage(event, 0); // Restart the Ticker + else + P036_data->P036_JumpToPageOfLine(event, LineNo - 1); // Start to display the selected page, function needs 65ms! # if P036_SEND_EVENTS if (sendEvents && bitRead(P036_FLAGS_0, P036_FLAG_EVENTS_FRAME_LINE) && (currentFrame != P036_data->currentFrameToDisplay)) { @@ -1349,7 +1354,7 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) # ifdef PLUGIN_036_DEBUG - if (eventId == P036_FLAG_EVENTS_FRAME_LINE) { + if (eventId == P036_EVENT_LINE) { String log; if (loglevelActiveFor(LOG_LEVEL_INFO) && diff --git a/src/src/PluginStructs/P036_data_struct.h b/src/src/PluginStructs/P036_data_struct.h index e36ddf866f..65a41510d1 100644 --- a/src/src/PluginStructs/P036_data_struct.h +++ b/src/src/PluginStructs/P036_data_struct.h @@ -22,7 +22,7 @@ // # define P036_FONT_CALC_LOG // Enable to add extra logging during font calculation (selection) // # define P036_SCROLL_CALC_LOG // Enable to add extra logging during scrolling calculation (selection) // # define P036_CHECK_HEAP // Enable to add extra logging during Plugin_036() -// # define P036_CHECK_INDIVIDUAL_FONT // /Enable to add extra logging for individual font calculation +// # define P036_CHECK_INDIVIDUAL_FONT // Enable to add extra logging for individual font calculation # ifndef P036_FEATURE_DISPLAY_PREVIEW # define P036_FEATURE_DISPLAY_PREVIEW 1 # endif // ifndef P036_FEATURE_DISPLAY_PREVIEW From 260a5884b473a75c92c54e54d4eaf119e386725b Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Mon, 28 Aug 2023 19:25:24 +0200 Subject: [PATCH 9/9] CHG: Code reduced if P036_ENABLE_TICKER is not used --- src/_P036_FrameOLED.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_P036_FrameOLED.ino b/src/_P036_FrameOLED.ino index cd6465c2a6..9d9efdc5b7 100644 --- a/src/_P036_FrameOLED.ino +++ b/src/_P036_FrameOLED.ino @@ -1340,9 +1340,11 @@ boolean Plugin_036(uint8_t function, struct EventStruct *event, String& string) if (P036_DisplayIsOn) { P036_data->bLineScrollEnabled=false; // disable scrolling temporary + # if P036_ENABLE_TICKER if (P036_data->bUseTicker) P036_data->P036_JumpToPage(event, 0); // Restart the Ticker else + # endif // if P036_ENABLE_TICKER P036_data->P036_JumpToPageOfLine(event, LineNo - 1); // Start to display the selected page, function needs 65ms! # if P036_SEND_EVENTS