From dccd13f0f198c5cbe4c6f1237a269472a45fdfc5 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Sun, 27 Aug 2023 16:13:14 +0200 Subject: [PATCH] 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 6dd1183fc..2be3e553c 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 97a056f4e..c02a834c3 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 40986a3d1..8978c48fe 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 978fc9cc1..16433c0a6 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 e040e63de..012cd7674 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 65636101b..7c16d3fe5 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 913609506..dd20d1a78 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 6aaeaf2b2..e36ddf866 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;