From 2972660cdda5ebafb03754dd9b84bbc5afed39e3 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Mon, 24 Jul 2023 15:19:03 +0200 Subject: [PATCH 01/30] [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 02/30] 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 03/30] 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 be3d329a0f2009c1a99923f647d9aaae5036f0cd Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 25 Jul 2023 23:22:29 +0200 Subject: [PATCH 04/30] [P028] Add detection-mode setting --- src/_P028_BME280.ino | 35 ++++++++++++++++++---- src/src/PluginStructs/P028_data_struct.cpp | 8 ++--- src/src/PluginStructs/P028_data_struct.h | 15 +++++++--- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/_P028_BME280.ino b/src/_P028_BME280.ino index 5613e3be17..355e1d723e 100644 --- a/src/_P028_BME280.ino +++ b/src/_P028_BME280.ino @@ -5,6 +5,11 @@ // #################### Plugin 028 BME280 I2C Temp/Hum/Barometric Pressure Sensor ####################### // ####################################################################################################### +/** Changelog: + * 2023-07-25 tonhuisman: Add setting to enable forcing the plugin into either BME280 or BMP280 mode, default is Auto-detect + * Add changelog + */ + # include "src/PluginStructs/P028_data_struct.h" // #include @@ -138,7 +143,7 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) if (nullptr != P028_data) { if (P028_data->sensorID != P028_data_struct::Unknown_DEVICE) { String detectedString = F("Detected: "); - detectedString += P028_data->getDeviceName(); + detectedString += P028_data->getDeviceName(P028_data->sensorID); addUnit(detectedString); } } @@ -157,6 +162,20 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) } addFormNote(offsetNote); + { + const __FlashStringHelper *detectOptionList[] = { + F("Auto"), + P028_data_struct::getDeviceName(P028_data_struct::BMx_ChipId::BME280_DEVICE), + P028_data_struct::getDeviceName(P028_data_struct::BMx_ChipId::BMP280_DEVICE), + }; + const int detectOptions[] = { + static_cast(P028_data_struct::BMx_DetectMode::Auto), + static_cast(P028_data_struct::BMx_DetectMode::BME280), + static_cast(P028_data_struct::BMx_DetectMode::BMP280), + }; + addFormSelector(F("Sensor model"), F("det"), 3, detectOptionList, detectOptions, P028_DETECTION_MODE); + } + success = true; break; } @@ -221,6 +240,7 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) P028_ALTITUDE = getFormItemInt(F("elev")); P028_TEMPERATURE_OFFSET = getFormItemInt(F("tempoffset")); P028_ERROR_STATE_OUTPUT = getFormItemInt(F("err")); + P028_DETECTION_MODE = getFormItemInt(F("det")); success = true; break; } @@ -261,7 +281,10 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) } else { P028_data->state = P028_data_struct::BMx_Values_read; - if (!P028_data->hasHumidity()) { + const P028_data_struct::BMx_DetectMode detectMode = static_cast(P028_DETECTION_MODE); + + if (((detectMode == P028_data_struct::BMx_DetectMode::Auto) && !P028_data->hasHumidity()) || + (detectMode == P028_data_struct::BMx_DetectMode::BMP280)) { // Patch the sensor type to output only the measured values. event->sensorType = Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO; } @@ -281,24 +304,24 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) String log; if (log.reserve(40)) { // Prevent re-allocation - log = P028_data->getDeviceName(); + log = P028_data->getDeviceName(P028_data->sensorID); log += F(": Address: "); log += formatToHex(P028_I2C_ADDRESS, 2); addLogMove(LOG_LEVEL_INFO, log); // addLogMove does also clear the string. - log = P028_data->getDeviceName(); + log = P028_data->getDeviceName(P028_data->sensorID); log += F(": Temperature: "); log += formatUserVarNoCheck(event->TaskIndex, 0); addLogMove(LOG_LEVEL_INFO, log); if (P028_data->hasHumidity()) { - log = P028_data->getDeviceName(); + log = P028_data->getDeviceName(P028_data->sensorID); log += F(": Humidity: "); log += formatUserVarNoCheck(event->TaskIndex, 1); addLogMove(LOG_LEVEL_INFO, log); } - log = P028_data->getDeviceName(); + log = P028_data->getDeviceName(P028_data->sensorID); log += F(": Barometric Pressure: "); log += formatUserVarNoCheck(event->TaskIndex, 2); addLogMove(LOG_LEVEL_INFO, log); diff --git a/src/src/PluginStructs/P028_data_struct.cpp b/src/src/PluginStructs/P028_data_struct.cpp index cd36b1db41..be6f0689c0 100644 --- a/src/src/PluginStructs/P028_data_struct.cpp +++ b/src/src/PluginStructs/P028_data_struct.cpp @@ -21,10 +21,10 @@ uint8_t P028_data_struct::get_control_settings() const { return sensorID == Unknown_DEVICE ? 0u : 0x93; // Oversampling: 8x P, 8x T, normal mode } -const __FlashStringHelper * P028_data_struct::getDeviceName() const { +const __FlashStringHelper * P028_data_struct::getDeviceName(BMx_ChipId sensorID) { switch (sensorID) { case BMP280_DEVICE_SAMPLE1: - case BMP280_DEVICE_SAMPLE2: return F("BMP280 sample"); + case BMP280_DEVICE_SAMPLE2: return F("sample BMP280"); case BMP280_DEVICE: return F("BMP280"); case BME280_DEVICE: return F("BME280"); default: return F("Unknown"); @@ -102,7 +102,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { log.reserve(120); // Prevent re-allocation - log = getDeviceName(); + log = getDeviceName(sensorID); log += ':'; } bool logAdded = false; @@ -210,7 +210,7 @@ bool P028_data_struct::check() { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("BMx280: Detected "); - log += getDeviceName(); + log += getDeviceName(sensorID); addLogMove(LOG_LEVEL_INFO, log); } } diff --git a/src/src/PluginStructs/P028_data_struct.h b/src/src/PluginStructs/P028_data_struct.h index 99643a1e27..c23762b0d4 100644 --- a/src/src/PluginStructs/P028_data_struct.h +++ b/src/src/PluginStructs/P028_data_struct.h @@ -61,6 +61,7 @@ # define P028_ALTITUDE PCONFIG(1) # define P028_TEMPERATURE_OFFSET PCONFIG(2) # define P028_ERROR_STATE_OUTPUT PCONFIG(3) +# define P028_DETECTION_MODE PCONFIG(4) struct P028_data_struct : public PluginTaskData_base { struct bme280_calib_data @@ -107,6 +108,12 @@ struct P028_data_struct : public PluginTaskData_base { BME280_DEVICE = 0x60 }; + enum BMx_DetectMode : uint8_t { + Auto = 0u, + BME280 = BMx_ChipId::BME280_DEVICE, + BMP280 = BMx_ChipId::BMP280_DEVICE, + }; + enum BMx_state { BMx_Uninitialized = 0, BMx_Initialized, @@ -119,7 +126,7 @@ struct P028_data_struct : public PluginTaskData_base { P028_data_struct(uint8_t addr, float tempOffset); - P028_data_struct() = delete; + P028_data_struct() = delete; virtual ~P028_data_struct() = default; private: @@ -130,11 +137,11 @@ struct P028_data_struct : public PluginTaskData_base { public: - const __FlashStringHelper* getDeviceName() const; + static const __FlashStringHelper* getDeviceName(BMx_ChipId sensorID); - bool hasHumidity() const; + bool hasHumidity() const; - bool initialized() const; + bool initialized() const; private: From a99a6397d3be1d96d052570d9d707dbbed8a2bc9 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 26 Jul 2023 21:59:49 +0200 Subject: [PATCH 05/30] [P028] Ignore humidity if Sensor model BMP280 is selected --- src/_P028_BME280.ino | 23 ++++++++++++---------- src/src/PluginStructs/P028_data_struct.cpp | 14 ++++++------- src/src/PluginStructs/P028_data_struct.h | 6 ++++-- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/_P028_BME280.ino b/src/_P028_BME280.ino index 355e1d723e..becf7d9fd3 100644 --- a/src/_P028_BME280.ino +++ b/src/_P028_BME280.ino @@ -6,6 +6,7 @@ // ####################################################################################################### /** Changelog: + * 2023-07-26 tonhuisman: Ignore all humidity data (and log messages) if BMP280 Sensor model is selected * 2023-07-25 tonhuisman: Add setting to enable forcing the plugin into either BME280 or BMP280 mode, default is Auto-detect * Add changelog */ @@ -103,7 +104,8 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) { const float tempOffset = P028_TEMPERATURE_OFFSET / 10.0f; initPluginTaskData(event->TaskIndex, - new (std::nothrow) P028_data_struct(P028_I2C_ADDRESS, tempOffset)); + new (std::nothrow) P028_data_struct(P028_I2C_ADDRESS, tempOffset, + static_cast(P028_DETECTION_MODE))); P028_data_struct *P028_data = static_cast(getPluginTaskData(event->TaskIndex)); @@ -143,7 +145,7 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) if (nullptr != P028_data) { if (P028_data->sensorID != P028_data_struct::Unknown_DEVICE) { String detectedString = F("Detected: "); - detectedString += P028_data->getDeviceName(P028_data->sensorID); + detectedString += P028_data_struct::getDeviceName(P028_data->sensorID); addUnit(detectedString); } } @@ -156,7 +158,8 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) String offsetNote = F("Offset in units of 0.1 degree Celsius"); if (nullptr != P028_data) { - if (P028_data->hasHumidity()) { + if ((P028_data_struct::BMx_DetectMode::BMP280 != static_cast(P028_DETECTION_MODE)) && + P028_data->hasHumidity()) { offsetNote += F(" (also correct humidity)"); } } @@ -283,8 +286,8 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) const P028_data_struct::BMx_DetectMode detectMode = static_cast(P028_DETECTION_MODE); - if (((detectMode == P028_data_struct::BMx_DetectMode::Auto) && !P028_data->hasHumidity()) || - (detectMode == P028_data_struct::BMx_DetectMode::BMP280)) { + if (((P028_data_struct::BMx_DetectMode::Auto == detectMode) && !P028_data->hasHumidity()) || + (P028_data_struct::BMx_DetectMode::BMP280 == detectMode)) { // Patch the sensor type to output only the measured values. event->sensorType = Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO; } @@ -304,24 +307,24 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) String log; if (log.reserve(40)) { // Prevent re-allocation - log = P028_data->getDeviceName(P028_data->sensorID); + log = P028_data_struct::getDeviceName(P028_data->sensorID); log += F(": Address: "); log += formatToHex(P028_I2C_ADDRESS, 2); addLogMove(LOG_LEVEL_INFO, log); // addLogMove does also clear the string. - log = P028_data->getDeviceName(P028_data->sensorID); + log = P028_data_struct::getDeviceName(P028_data->sensorID); log += F(": Temperature: "); log += formatUserVarNoCheck(event->TaskIndex, 0); addLogMove(LOG_LEVEL_INFO, log); - if (P028_data->hasHumidity()) { - log = P028_data->getDeviceName(P028_data->sensorID); + if ((P028_data_struct::BMx_DetectMode::BMP280 != detectMode) && P028_data->hasHumidity()) { + log = P028_data_struct::getDeviceName(P028_data->sensorID); log += F(": Humidity: "); log += formatUserVarNoCheck(event->TaskIndex, 1); addLogMove(LOG_LEVEL_INFO, log); } - log = P028_data->getDeviceName(P028_data->sensorID); + log = P028_data_struct::getDeviceName(P028_data->sensorID); log += F(": Barometric Pressure: "); log += formatUserVarNoCheck(event->TaskIndex, 2); addLogMove(LOG_LEVEL_INFO, log); diff --git a/src/src/PluginStructs/P028_data_struct.cpp b/src/src/PluginStructs/P028_data_struct.cpp index be6f0689c0..8a4e2be395 100644 --- a/src/src/PluginStructs/P028_data_struct.cpp +++ b/src/src/PluginStructs/P028_data_struct.cpp @@ -9,8 +9,8 @@ // 1 second = 63% of the time needed to perform a measurement. # define P028_MEASUREMENT_TIMEOUT 1.587f -P028_data_struct::P028_data_struct(uint8_t addr, float tempOffset) : - i2cAddress(addr), temp_offset(tempOffset) {} +P028_data_struct::P028_data_struct(uint8_t addr, float tempOffset, BMx_DetectMode detectMode) : + i2cAddress(addr), temp_offset(tempOffset), _detectMode(detectMode) {} uint8_t P028_data_struct::get_config_settings() const { @@ -108,7 +108,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { bool logAdded = false; # endif // ifndef LIMIT_BUILD_SIZE - if (hasHumidity()) { + if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { // Apply half of the temp offset, to correct the dew point offset. // The sensor is warmer than the surrounding air, which has effect on the perceived humidity. last_dew_temp_val = compute_dew_point_temp(last_temp_val + (temp_offset / 2.0f), last_hum_val); @@ -128,7 +128,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { } # endif // ifndef LIMIT_BUILD_SIZE - if (hasHumidity()) { + if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { # ifndef LIMIT_BUILD_SIZE if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -172,7 +172,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { # ifndef LIMIT_BUILD_SIZE - if (hasHumidity()) { + if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { log += F(" dew point: "); log += last_dew_temp_val; @@ -273,7 +273,7 @@ void P028_data_struct::readCoefficients() calib.dig_P8 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P8); calib.dig_P9 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P9); - if (hasHumidity()) { + if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { calib.dig_H1 = I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H1); calib.dig_H2 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_H2); calib.dig_H3 = I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H3); @@ -364,7 +364,7 @@ float P028_data_struct::readPressure() const float P028_data_struct::readHumidity() const { - if (!hasHumidity()) { + if (!hasHumidity() || (BMx_DetectMode::BMP280 == _detectMode)) { // No support for humidity return 0.0f; } diff --git a/src/src/PluginStructs/P028_data_struct.h b/src/src/PluginStructs/P028_data_struct.h index c23762b0d4..058f46ba85 100644 --- a/src/src/PluginStructs/P028_data_struct.h +++ b/src/src/PluginStructs/P028_data_struct.h @@ -124,8 +124,9 @@ struct P028_data_struct : public PluginTaskData_base { }; - P028_data_struct(uint8_t addr, - float tempOffset); + P028_data_struct(uint8_t addr, + float tempOffset, + BMx_DetectMode detectMode); P028_data_struct() = delete; virtual ~P028_data_struct() = default; @@ -197,6 +198,7 @@ struct P028_data_struct : public PluginTaskData_base { bme280_uncomp_data uncompensated; bme280_calib_data calib; + BMx_DetectMode _detectMode; unsigned long last_measurement = 0; From 9daceb776226cecaae1e01b056f9f0a29b0f1f8b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 27 Jul 2023 22:27:43 +0200 Subject: [PATCH 06/30] [Devices] Add event PLUGIN_WEBFORM_LOAD_ALWAYS that is also called for remote data-feed devices --- src/src/DataTypes/ESPEasy_plugin_functions.h | 1 + src/src/Globals/Plugins.cpp | 4 +++- src/src/WebServer/DevicesPage.cpp | 8 ++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/src/DataTypes/ESPEasy_plugin_functions.h b/src/src/DataTypes/ESPEasy_plugin_functions.h index f73fe81903..0a4c8c8c3a 100644 --- a/src/src/DataTypes/ESPEasy_plugin_functions.h +++ b/src/src/DataTypes/ESPEasy_plugin_functions.h @@ -60,6 +60,7 @@ #define PLUGIN_PROCESS_CONTROLLER_DATA 48 // Can be called from the controller to signal the plugin to generate (or handle) sending the data. #define PLUGIN_PRIORITY_INIT_ALL 49 // Pre-initialize all plugins that are set to PowerManager priority (not implemented in plugins) #define PLUGIN_PRIORITY_INIT 50 // Pre-initialize a singe plugins that is set to PowerManager priority +#define PLUGIN_WEBFORM_LOAD_ALWAYS 51 // Loaded *after* PLUGIN_WEBFORM_LOAD, also shown for remote data-feed devices diff --git a/src/src/Globals/Plugins.cpp b/src/src/Globals/Plugins.cpp index bc872c14cb..04b852e08a 100644 --- a/src/src/Globals/Plugins.cpp +++ b/src/src/Globals/Plugins.cpp @@ -630,6 +630,7 @@ bool PluginCall(uint8_t Function, struct EventStruct *event, String& str) case PLUGIN_INIT: case PLUGIN_EXIT: case PLUGIN_WEBFORM_LOAD: + case PLUGIN_WEBFORM_LOAD_ALWAYS: case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: case PLUGIN_READ: case PLUGIN_GET_PACKED_RAW_DATA: @@ -654,7 +655,7 @@ bool PluginCall(uint8_t Function, struct EventStruct *event, String& str) // Only exception is when ErrorStateValues is needed. // Therefore only need to call LoadTaskSettings for those tasks with ErrorStateValues LoadTaskSettings(event->TaskIndex); - } else if (Function == PLUGIN_INIT || Function == PLUGIN_WEBFORM_LOAD) { + } else if (Function == PLUGIN_INIT || Function == PLUGIN_WEBFORM_LOAD || Function == PLUGIN_WEBFORM_LOAD_ALWAYS) { // LoadTaskSettings may call PLUGIN_GET_DEVICEVALUENAMES. LoadTaskSettings(event->TaskIndex); } @@ -824,6 +825,7 @@ bool PluginCall(uint8_t Function, struct EventStruct *event, String& str) if (Function == PLUGIN_GET_DEVICEVALUENAMES || Function == PLUGIN_WEBFORM_SAVE || Function == PLUGIN_WEBFORM_LOAD || + Function == PLUGIN_WEBFORM_LOAD_ALWAYS || Function == PLUGIN_SET_DEFAULTS || Function == PLUGIN_INIT_VALUE_RANGES || Function == PLUGIN_WEBFORM_SHOW_SERIAL_PARAMS diff --git a/src/src/WebServer/DevicesPage.cpp b/src/src/WebServer/DevicesPage.cpp index 7aa21808d2..8c7f7d9f70 100644 --- a/src/src/WebServer/DevicesPage.cpp +++ b/src/src/WebServer/DevicesPage.cpp @@ -970,10 +970,10 @@ void handle_devices_TaskSettingsPage(taskIndex_t taskIndex, uint8_t page) addFormSubHeader(F("Device Settings")); } + String webformLoadString; + struct EventStruct TempEvent(taskIndex); // add plugins content if (Settings.TaskDeviceDataFeed[taskIndex] == 0) { // only show additional config for local connected sensors - String webformLoadString; - struct EventStruct TempEvent(taskIndex); PluginCall(PLUGIN_WEBFORM_LOAD, &TempEvent, webformLoadString); if (webformLoadString.length() > 0) { @@ -982,6 +982,8 @@ void handle_devices_TaskSettingsPage(taskIndex_t taskIndex, uint8_t page) errorMessage += F(": Bug in PLUGIN_WEBFORM_LOAD, should not append to string, use addHtml() instead"); addHtmlError(errorMessage); } + + PluginCall(PLUGIN_WEBFORM_LOAD_ALWAYS, &TempEvent, webformLoadString); // Load settings also useful for remote-datafeed devices } else { #if FEATURE_ESPEASY_P2P @@ -1001,6 +1003,8 @@ void handle_devices_TaskSettingsPage(taskIndex_t taskIndex, uint8_t page) } addFormNote(F("0 = disable remote feed, 255 = broadcast")); // FIXME TD-er: Must verify if broadcast can be set. #endif + + PluginCall(PLUGIN_WEBFORM_LOAD_ALWAYS, &TempEvent, webformLoadString); // Load settings also useful for remote-datafeed devices } devicePage_show_output_data_type(taskIndex, DeviceIndex); From 13e6cbe5fd8f8b6ef52987ab93fa41451471113c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 27 Jul 2023 22:29:18 +0200 Subject: [PATCH 07/30] [P028] Fix VType matching for remote data-feed, implement new WEBFORM_LOAD_ALWAYS event --- src/_P028_BME280.ino | 52 +++++++++++++++------- src/src/PluginStructs/P028_data_struct.cpp | 14 +++--- src/src/PluginStructs/P028_data_struct.h | 6 +-- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/_P028_BME280.ino b/src/_P028_BME280.ino index becf7d9fd3..027bc2d4f8 100644 --- a/src/_P028_BME280.ino +++ b/src/_P028_BME280.ino @@ -6,6 +6,8 @@ // ####################################################################################################### /** Changelog: + * 2023-07-27 tonhuisman: Revert most below changes and implement PLUGIN_GET_DEVICEVTYPE so the P2P controller validates against the correct + * setting. Setting is only available if a remote data-feed is active, and offers BME280 and BMP280 options only. * 2023-07-26 tonhuisman: Ignore all humidity data (and log messages) if BMP280 Sensor model is selected * 2023-07-25 tonhuisman: Add setting to enable forcing the plugin into either BME280 or BMP280 mode, default is Auto-detect * Add changelog @@ -100,12 +102,26 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) break; } + case PLUGIN_GET_DEVICEVTYPE: + { + const P028_data_struct::BMx_DetectMode detectMode = static_cast(P028_DETECTION_MODE); + + // We want to configure this only when a remote data-feed is used + if ((Settings.TaskDeviceDataFeed[event->TaskIndex] != 0) && (P028_data_struct::BMx_DetectMode::BMP280 == detectMode)) { + // Patch the sensor type to output only the measured values, and/or match with a P2P remote sensor + event->sensorType = Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO; + event->idx = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO); + } + + success = true; + break; + } + case PLUGIN_INIT: { const float tempOffset = P028_TEMPERATURE_OFFSET / 10.0f; initPluginTaskData(event->TaskIndex, - new (std::nothrow) P028_data_struct(P028_I2C_ADDRESS, tempOffset, - static_cast(P028_DETECTION_MODE))); + new (std::nothrow) P028_data_struct(P028_I2C_ADDRESS, tempOffset)); P028_data_struct *P028_data = static_cast(getPluginTaskData(event->TaskIndex)); @@ -165,21 +181,26 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) } addFormNote(offsetNote); - { + success = true; + break; + } + + case PLUGIN_WEBFORM_LOAD_ALWAYS: + { + if (Settings.TaskDeviceDataFeed[event->TaskIndex] != 0) { // We want to configure this *only* when a remote data-feed is used const __FlashStringHelper *detectOptionList[] = { - F("Auto"), P028_data_struct::getDeviceName(P028_data_struct::BMx_ChipId::BME280_DEVICE), P028_data_struct::getDeviceName(P028_data_struct::BMx_ChipId::BMP280_DEVICE), }; const int detectOptions[] = { - static_cast(P028_data_struct::BMx_DetectMode::Auto), static_cast(P028_data_struct::BMx_DetectMode::BME280), static_cast(P028_data_struct::BMx_DetectMode::BMP280), }; - addFormSelector(F("Sensor model"), F("det"), 3, detectOptionList, detectOptions, P028_DETECTION_MODE); - } + addFormSelector(F("Output values mode"), F("det"), 2, detectOptionList, detectOptions, P028_DETECTION_MODE); + addFormNote(F("'Auto' is the suggested setting, see documentation (i)")); - success = true; + success = true; + } break; } @@ -243,8 +264,11 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) P028_ALTITUDE = getFormItemInt(F("elev")); P028_TEMPERATURE_OFFSET = getFormItemInt(F("tempoffset")); P028_ERROR_STATE_OUTPUT = getFormItemInt(F("err")); - P028_DETECTION_MODE = getFormItemInt(F("det")); - success = true; + + if (Settings.TaskDeviceDataFeed[event->TaskIndex] != 0) { // We want to configure this only when a remote data-feed is used + P028_DETECTION_MODE = getFormItemInt(F("det")); + } + success = true; break; } case PLUGIN_ONCE_A_SECOND: @@ -284,12 +308,10 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) } else { P028_data->state = P028_data_struct::BMx_Values_read; - const P028_data_struct::BMx_DetectMode detectMode = static_cast(P028_DETECTION_MODE); - - if (((P028_data_struct::BMx_DetectMode::Auto == detectMode) && !P028_data->hasHumidity()) || - (P028_data_struct::BMx_DetectMode::BMP280 == detectMode)) { + if (!P028_data->hasHumidity()) { // Patch the sensor type to output only the measured values. event->sensorType = Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO; + event->idx = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_TEMP_EMPTY_BARO); } UserVar[event->BaseVarIndex] = ExtraTaskSettings.checkAllowedRange(0, P028_data->last_temp_val); UserVar[event->BaseVarIndex + 1] = P028_data->last_hum_val; @@ -318,7 +340,7 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) log += formatUserVarNoCheck(event->TaskIndex, 0); addLogMove(LOG_LEVEL_INFO, log); - if ((P028_data_struct::BMx_DetectMode::BMP280 != detectMode) && P028_data->hasHumidity()) { + if (P028_data->hasHumidity()) { log = P028_data_struct::getDeviceName(P028_data->sensorID); log += F(": Humidity: "); log += formatUserVarNoCheck(event->TaskIndex, 1); diff --git a/src/src/PluginStructs/P028_data_struct.cpp b/src/src/PluginStructs/P028_data_struct.cpp index 8a4e2be395..be6f0689c0 100644 --- a/src/src/PluginStructs/P028_data_struct.cpp +++ b/src/src/PluginStructs/P028_data_struct.cpp @@ -9,8 +9,8 @@ // 1 second = 63% of the time needed to perform a measurement. # define P028_MEASUREMENT_TIMEOUT 1.587f -P028_data_struct::P028_data_struct(uint8_t addr, float tempOffset, BMx_DetectMode detectMode) : - i2cAddress(addr), temp_offset(tempOffset), _detectMode(detectMode) {} +P028_data_struct::P028_data_struct(uint8_t addr, float tempOffset) : + i2cAddress(addr), temp_offset(tempOffset) {} uint8_t P028_data_struct::get_config_settings() const { @@ -108,7 +108,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { bool logAdded = false; # endif // ifndef LIMIT_BUILD_SIZE - if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { + if (hasHumidity()) { // Apply half of the temp offset, to correct the dew point offset. // The sensor is warmer than the surrounding air, which has effect on the perceived humidity. last_dew_temp_val = compute_dew_point_temp(last_temp_val + (temp_offset / 2.0f), last_hum_val); @@ -128,7 +128,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { } # endif // ifndef LIMIT_BUILD_SIZE - if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { + if (hasHumidity()) { # ifndef LIMIT_BUILD_SIZE if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -172,7 +172,7 @@ bool P028_data_struct::updateMeasurements(taskIndex_t task_index) { # ifndef LIMIT_BUILD_SIZE - if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { + if (hasHumidity()) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { log += F(" dew point: "); log += last_dew_temp_val; @@ -273,7 +273,7 @@ void P028_data_struct::readCoefficients() calib.dig_P8 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P8); calib.dig_P9 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_P9); - if ((BMx_DetectMode::BMP280 != _detectMode) && hasHumidity()) { + if (hasHumidity()) { calib.dig_H1 = I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H1); calib.dig_H2 = I2C_readS16_LE_reg(i2cAddress, BMx280_REGISTER_DIG_H2); calib.dig_H3 = I2C_read8_reg(i2cAddress, BMx280_REGISTER_DIG_H3); @@ -364,7 +364,7 @@ float P028_data_struct::readPressure() const float P028_data_struct::readHumidity() const { - if (!hasHumidity() || (BMx_DetectMode::BMP280 == _detectMode)) { + if (!hasHumidity()) { // No support for humidity return 0.0f; } diff --git a/src/src/PluginStructs/P028_data_struct.h b/src/src/PluginStructs/P028_data_struct.h index 058f46ba85..c23762b0d4 100644 --- a/src/src/PluginStructs/P028_data_struct.h +++ b/src/src/PluginStructs/P028_data_struct.h @@ -124,9 +124,8 @@ struct P028_data_struct : public PluginTaskData_base { }; - P028_data_struct(uint8_t addr, - float tempOffset, - BMx_DetectMode detectMode); + P028_data_struct(uint8_t addr, + float tempOffset); P028_data_struct() = delete; virtual ~P028_data_struct() = default; @@ -198,7 +197,6 @@ struct P028_data_struct : public PluginTaskData_base { bme280_uncomp_data uncompensated; bme280_calib_data calib; - BMx_DetectMode _detectMode; unsigned long last_measurement = 0; From 3080b47f647a94e73144f7311d1e11d0f8b1bd3a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 29 Jul 2023 12:24:48 +0200 Subject: [PATCH 08/30] [P028] Update documentation --- docs/source/Plugin/P028.rst | 27 +++++++++++++++++++ docs/source/Plugin/P028_DataSource.png | Bin 0 -> 6811 bytes docs/source/Plugin/P028_OutputValuesMode.png | Bin 0 -> 13997 bytes src/_P028_BME280.ino | 1 - src/src/WebServer/DevicesPage.cpp | 2 +- 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 docs/source/Plugin/P028_DataSource.png create mode 100644 docs/source/Plugin/P028_OutputValuesMode.png diff --git a/docs/source/Plugin/P028.rst b/docs/source/Plugin/P028.rst index c835fcea99..80c4c979d0 100644 --- a/docs/source/Plugin/P028.rst +++ b/docs/source/Plugin/P028.rst @@ -99,11 +99,38 @@ Single event with all values, Send to Controller and Interval settings are stand * **Interval** By default, Interval will be set to 60 sec. The minimum value allowed is 1 sec. + +Data Source +^^^^^^^^^^^ + +.. note:: The **Data Source** section is only available when the task is configured to receive data from a remote node! + +When using :ref:`c013_page` and having multiple ESP nodes using the same ``ESPEasy p2p UDP port`` (see the Tools/Advanced page, default: 8266, IANA registered), you can receive values from a remote node with the same plugin active. How to configure this is documented in the P2P Controller page. + +In a regular configuration, having the sensor connected locally, the plugin auto-detects what type of sensor is used, either BME280 or BMP280, and auto-adjusts the values supported. When receiving data from the P2P network, this 'setting' is verified and must match on the receiving end of the P2P connection. This information is not included in the P2P protocol data, so a configuration option is available, only shown if the task is configured to receive remote data. + +.. image:: P028_DataSource.png + +* **Remote Unit**: Shows the unit number and name the data is received from. + +* **Output values mode**: Allows selection of the sensor that is installed on the sending unit (node). + +.. image:: P028_OutputValuesMode.png + +Available options: + +* *BME280*: Sending node has a BME280 sensor, will provide the Temperature, Humidity and Pressure values. + +* *BMP280*: Sending node has a BMP280 sensor, will provide the Temperature and Pressure values. + + Values ^^^^^^ The measured values are available in ``Temperature``, ``Humidity`` and ``Pressure``. A formula can be set to recalculate. The number of decimals is by default set to 2, and can be set to 0 for ``Humidity`` and ``Pressure``, as no decimals are provided from the measurement. +.. note:: When a BMP280 sensor, that provides only ``Temperature`` and ``Pressure`` data, is connected, the ``Humidity`` value will still be visible, but show 0. + .. Commands available .. ^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/Plugin/P028_DataSource.png b/docs/source/Plugin/P028_DataSource.png new file mode 100644 index 0000000000000000000000000000000000000000..4c7ac0bfe0e0b6a6c98ebe902781ffe5e5fd3269 GIT binary patch literal 6811 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 lWO4`Xi*gX_eMUzQkG!bwRJAIa&i>{6FzcIf?)P literal 0 HcmV?d00001 diff --git a/src/_P028_BME280.ino b/src/_P028_BME280.ino index 027bc2d4f8..08189ea94c 100644 --- a/src/_P028_BME280.ino +++ b/src/_P028_BME280.ino @@ -197,7 +197,6 @@ boolean Plugin_028(uint8_t function, struct EventStruct *event, String& string) static_cast(P028_data_struct::BMx_DetectMode::BMP280), }; addFormSelector(F("Output values mode"), F("det"), 2, detectOptionList, detectOptions, P028_DETECTION_MODE); - addFormNote(F("'Auto' is the suggested setting, see documentation (i)")); success = true; } diff --git a/src/src/WebServer/DevicesPage.cpp b/src/src/WebServer/DevicesPage.cpp index 8c7f7d9f70..3396e8ef0e 100644 --- a/src/src/WebServer/DevicesPage.cpp +++ b/src/src/WebServer/DevicesPage.cpp @@ -989,7 +989,7 @@ void handle_devices_TaskSettingsPage(taskIndex_t taskIndex, uint8_t page) #if FEATURE_ESPEASY_P2P // Show remote feed information. addFormSubHeader(F("Data Source")); - uint8_t remoteUnit = Settings.TaskDeviceDataFeed[taskIndex]; + const uint8_t remoteUnit = Settings.TaskDeviceDataFeed[taskIndex]; addFormNumericBox(F("Remote Unit"), F("RemoteUnit"), remoteUnit, 0, 255); if (remoteUnit != 255) { From 291c967bcdeb1bbf5314da465bf796828171a047 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 29 Jul 2023 16:48:14 +0200 Subject: [PATCH 09/30] [Controllers] Show correct data (or nothing) for controllers without Host or Port settings --- src/_C013.cpp | 13 +++++++++++-- src/_C016.cpp | 6 ++++++ src/src/WebServer/ControllerPage.cpp | 23 +++++++++++++++-------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/_C013.cpp b/src/_C013.cpp index c8face7fde..4421213317 100644 --- a/src/_C013.cpp +++ b/src/_C013.cpp @@ -48,6 +48,7 @@ bool CPlugin_013(CPlugin::Function function, struct EventStruct *event, String& Protocol[protocolCount].usesTemplate = false; Protocol[protocolCount].usesAccount = false; Protocol[protocolCount].usesPassword = false; + Protocol[protocolCount].usesHost = false; Protocol[protocolCount].defaultPort = 8266; Protocol[protocolCount].usesID = false; Protocol[protocolCount].Custom = true; @@ -79,6 +80,12 @@ bool CPlugin_013(CPlugin::Function function, struct EventStruct *event, String& break; } + case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG: + { + string = F("-"); + break; + } + /* case CPlugin::Function::CPLUGIN_FLUSH: { @@ -157,7 +164,8 @@ void C013_SendUDPTaskData(struct EventStruct *event, uint8_t destUnit, uint8_t d // For example sending different sensor type data from one dummy to another is probably not going to work well dataReply.sensorType = event->getSensorType(); - const TaskValues_Data_t* taskValues = UserVar.getTaskValues_Data(event->TaskIndex); + const TaskValues_Data_t *taskValues = UserVar.getTaskValues_Data(event->TaskIndex); + if (taskValues != nullptr) { for (taskVarIndex_t x = 0; x < VARS_PER_TASK; ++x) { @@ -319,7 +327,8 @@ void C013_Receive(struct EventStruct *event) { const Sensor_VType sensorType = TempEvent.getSensorType(); if (dataReply.matchesSensorType(sensorType)) { - TaskValues_Data_t * taskValues = UserVar.getTaskValues_Data(dataReply.destTaskIndex); + TaskValues_Data_t *taskValues = UserVar.getTaskValues_Data(dataReply.destTaskIndex); + if (taskValues != nullptr) { for (taskVarIndex_t x = 0; x < VARS_PER_TASK; ++x) { diff --git a/src/_C016.cpp b/src/_C016.cpp index 27d2102305..48f0deb985 100644 --- a/src/_C016.cpp +++ b/src/_C016.cpp @@ -150,6 +150,12 @@ bool CPlugin_016(CPlugin::Function function, struct EventStruct *event, String& break; } + case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG: + { + string = F("-"); + break; + } + default: break; } diff --git a/src/src/WebServer/ControllerPage.cpp b/src/src/WebServer/ControllerPage.cpp index deb0faf15e..356f7b9c91 100644 --- a/src/src/WebServer/ControllerPage.cpp +++ b/src/src/WebServer/ControllerPage.cpp @@ -58,6 +58,7 @@ void handle_controllers() { // Otherwise the checksum will fail and settings will be saved too often. memset(&ControllerSettings, 0, sizeof(ControllerSettingsStruct)); ControllerSettings.reset(); + if (Settings.Protocol[controllerindex] != protocol) { // Protocol has changed. @@ -146,7 +147,7 @@ void handle_controllers_clearLoadDefaults(uint8_t controllerindex, ControllerSet struct EventStruct TempEvent; // Hand over the controller settings in the Data pointer, so the controller can set some defaults. - TempEvent.Data = (uint8_t*)(&ControllerSettings); + TempEvent.Data = (uint8_t *)(&ControllerSettings); if (Protocol[ProtocolIndex].usesTemplate) { String dummy; @@ -245,8 +246,8 @@ void handle_controllers_ShowAllControllersTable() html_TD(); addHtml(getCPluginNameFromCPluginID(Settings.Protocol[x])); html_TD(); + const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(x); { - const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(x); String hostDescription; CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG, 0, hostDescription); @@ -258,7 +259,9 @@ void handle_controllers_ShowAllControllersTable() } html_TD(); - addHtmlInt(ControllerSettings.Port); + if ((INVALID_PROTOCOL_INDEX == ProtocolIndex) || (Protocol[ProtocolIndex].usesPort)) { + addHtmlInt(13 != Settings.Protocol[x] ? ControllerSettings.Port : Settings.UDPPort); // P2P exception + } } else { html_TD(3); @@ -329,6 +332,7 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_IP); } } + if (Protocol[ProtocolIndex].usesPort) { addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_PORT); } @@ -364,6 +368,7 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex if (Protocol[ProtocolIndex].usesSampleSets) { addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_SAMPLE_SET_INITIATOR); } + if (Protocol[ProtocolIndex].allowLocalSystemTime) { addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_USE_LOCAL_SYSTEM_TIME); } @@ -386,7 +391,8 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex { addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_PASS); } - #if FEATURE_MQTT + # if FEATURE_MQTT + if (Protocol[ProtocolIndex].usesMQTT) { addTableSeparator(F("MQTT"), 2, 3); @@ -397,7 +403,7 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex addFormNote(F("Updated on load of this page")); addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_RETAINFLAG); } - #endif // if FEATURE_MQTT + # endif // if FEATURE_MQTT if (Protocol[ProtocolIndex].usesTemplate || Protocol[ProtocolIndex].usesMQTT) @@ -405,7 +411,8 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_SUBSCRIBE); addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_PUBLISH); } - #if FEATURE_MQTT + # if FEATURE_MQTT + if (Protocol[ProtocolIndex].usesMQTT) { addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_LWT_TOPIC); @@ -415,7 +422,7 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_WILL_RETAIN); addControllerParameterForm(ControllerSettings, controllerindex, ControllerSettingsStruct::CONTROLLER_CLEAN_SESSION); } - #endif // if FEATURE_MQTT + # endif // if FEATURE_MQTT } } @@ -448,4 +455,4 @@ void handle_controllers_ControllerSettingsPage(controllerIndex_t controllerindex html_end_form(); } -#endif // ifdef WEBSERVER_CONTROLLERS \ No newline at end of file +#endif // ifdef WEBSERVER_CONTROLLERS From c74434f9c4e99335e86d76cc4427ce32429d9f31 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 7 Aug 2023 13:14:45 +0200 Subject: [PATCH 10/30] [Controllers] Code improvement --- src/src/WebServer/ControllerPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/WebServer/ControllerPage.cpp b/src/src/WebServer/ControllerPage.cpp index 356f7b9c91..2ac9f908f0 100644 --- a/src/src/WebServer/ControllerPage.cpp +++ b/src/src/WebServer/ControllerPage.cpp @@ -260,7 +260,7 @@ void handle_controllers_ShowAllControllersTable() html_TD(); if ((INVALID_PROTOCOL_INDEX == ProtocolIndex) || (Protocol[ProtocolIndex].usesPort)) { - addHtmlInt(13 != Settings.Protocol[x] ? ControllerSettings.Port : Settings.UDPPort); // P2P exception + addHtmlInt(13 == Settings.Protocol[x] ? Settings.UDPPort : ControllerSettings.Port); // P2P/C013 exception } } else { From 87afa6142a8bd950fec71f78f981b5d8183424d0 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Thu, 10 Aug 2023 22:37:10 +0200 Subject: [PATCH 11/30] 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 12/30] 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 0be0f49d1b384daf7eb506d53e7e3d67e2113e08 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 13 Aug 2023 20:29:07 +0200 Subject: [PATCH 13/30] [P104] Add Dot subcommand to draw individual dots in a zone --- src/_P104_max7219_Dotmatrix.ino | 11 ++- src/src/PluginStructs/P104_data_struct.cpp | 90 ++++++++++++++++++---- src/src/PluginStructs/P104_data_struct.h | 33 +++++--- 3 files changed, 104 insertions(+), 30 deletions(-) diff --git a/src/_P104_max7219_Dotmatrix.ino b/src/_P104_max7219_Dotmatrix.ino index 68f367fd1f..7019183dab 100644 --- a/src/_P104_max7219_Dotmatrix.ino +++ b/src/_P104_max7219_Dotmatrix.ino @@ -66,8 +66,16 @@ // 2: dotted line, alternating, only if the bar is wider than 1 pixel // Up to 8 graph-strings can be provided and must be separated by a pipe | // The bar width is determined by the number of graph-strings +// dot,,,[,0][,...] : Draw dot(s) at the row/column positions provided, adding a 0 after a coordinate will turn that dot off. +// Default is to turn a dot On (no argument needed). +// Row/col must fit within the zone (r = 1..8, c = 1..8 * modules), double-height parts are separate zones! +// Display of the selected zone is suspended until all provided dots are drawn. No quotes are needed around +// the coordinate set. // // History: +// 2023-08-13 tonhuisman: Add Dot subcommand for pixel-drawing in a zone. Can be applied on any type of zone (so can be overwritten by the +// original content when that's updated...) +// Set default Hardware type to FC16, as that's the most used for modules found on Aliexpress // 2023-03-07 tonhuisman: Parse text to display without trimming off leading and trailing spaces // 2022-08-12 tonhuisman: Remove [DEVELOPMENT] tag // 2021-10-03 tonhuisman: Add Inverted option per zone @@ -138,7 +146,8 @@ boolean Plugin_104(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_SET_DEFAULTS: { - CONFIG_PORT = -1; + CONFIG_PORT = -1; + P104_CONFIG_HARDWARETYPE = static_cast(MD_MAX72XX::moduleType_t::FC16_HW); break; } diff --git a/src/src/PluginStructs/P104_data_struct.cpp b/src/src/PluginStructs/P104_data_struct.cpp index 867958608e..770a480411 100644 --- a/src/src/PluginStructs/P104_data_struct.cpp +++ b/src/src/PluginStructs/P104_data_struct.cpp @@ -44,12 +44,12 @@ P104_data_struct::P104_data_struct(MD_MAX72XX::moduleType_t _mod, * Destructor ******************************/ P104_data_struct::~P104_data_struct() { - # ifdef P104_USE_BAR_GRAPH + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) if (nullptr != pM) { pM = nullptr; // Not created here, only reset } - # endif // ifdef P104_USE_BAR_GRAPH + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) if (nullptr != P) { // P->~MD_Parola(); // Call destructor directly, as delete of the object fails miserably @@ -73,9 +73,9 @@ bool P104_data_struct::begin() { addLog(LOG_LEVEL_INFO, F("dotmatrix: begin() called")); # endif // ifdef P104_DEBUG P->begin(expectedZones); - # ifdef P104_USE_BAR_GRAPH + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) pM = P->getGraphicObject(); - # endif // ifdef P104_USE_BAR_GRAPH + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) return true; } return false; @@ -373,10 +373,10 @@ void P104_data_struct::configureZones() { if (it->zone <= expectedZones) { zoneOffset += it->offset; P->setZone(currentZone, zoneOffset, zoneOffset + it->size - 1); - # ifdef P104_USE_BAR_GRAPH + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) it->_startModule = zoneOffset; P->getDisplayExtent(currentZone, it->_lower, it->_upper); - # endif // ifdef P104_USE_BAR_GRAPH + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) zoneOffset += it->size; switch (it->font) { @@ -590,7 +590,7 @@ void P104_data_struct::updateZone(uint8_t zone, } } -# ifdef P104_USE_BAR_GRAPH +# if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) /*********************************************** * Enable/Disable updating a range of modules @@ -601,6 +601,10 @@ void P104_data_struct::modulesOnOff(uint8_t start, uint8_t end, MD_MAX72XX::cont } } +# endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + +# ifdef P104_USE_BAR_GRAPH + /******************************************************** * draw a single bar-graph, arguments already adjusted for direction *******************************************************/ @@ -841,6 +845,50 @@ void P104_data_struct::displayBarGraph(uint8_t zone, # endif // ifdef P104_USE_BAR_GRAPH +# ifdef P104_USE_DOT_SET +void P104_data_struct::displayDots(uint8_t zone, + const P104_zone_struct& zstruct, + const String & dots) { + if ((nullptr == P) || (nullptr == pM) || dots.isEmpty()) { return; } + { + uint8_t idx = 0; + int row; + int col; + String sRow; + String sCol; + String sOn_off; + bool on_off = true; + modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules + P->setIntensity(zstruct.zone - 1, zstruct.brightness); // don't forget to set the brightness + sRow = parseString(dots, idx + 1); + sCol = parseString(dots, idx + 2); + sOn_off = parseString(dots, idx + 3); + + while (!sRow.isEmpty() && !sCol.isEmpty()) { + on_off = true; // Default On + + if (validIntFromString(sRow, row) && + validIntFromString(sCol, col) && + (row > 0) && ((row - 1) < 8) && + (col > 0) && ((col - 1) <= (zstruct._upper - zstruct._lower))) { // Valid coordinates? + if (equals(sOn_off, F("0"))) { // Dot On is the default + on_off = false; + idx++; // 3rd argument used + } + pM->setPoint(row - 1, zstruct._upper - (col - 1), on_off); // Reverse layout + } + idx += 2; // Skip to next argument set + sRow = parseString(dots, idx + 1); + sCol = parseString(dots, idx + 2); + sOn_off = parseString(dots, idx + 3); + } + + modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules + } +} + +# endif // ifdef P104_USE_DOT_SET + /************************************************** * Check if an animation is available in the current build *************************************************/ @@ -965,7 +1013,7 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, (static_cast(zoneIndex) <= zones.size())) { // subcommands are processed in the same order as they are presented in the UI for (auto it = zones.begin(); it != zones.end() && !success; ++it) { - if ((it->zone == zoneIndex)) { // This zone + if ((it->zone == zoneIndex)) { // This zone if (equals(sub, F("clear"))) { // subcommand: clear, P->displayClear(zoneIndex - 1); success = true; @@ -990,11 +1038,11 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, } # endif // ifdef P104_USE_COMMANDS - if ((equals(sub, F("txt")) || // subcommand: [set]txt,, (only - equals(sub, F("settxt"))) && // allowed for zones with Text content) + if ((equals(sub, F("txt")) || // subcommand: [set]txt,, (only + equals(sub, F("settxt"))) && // allowed for zones with Text content) ((it->content == P104_CONTENT_TEXT) || (it->content == P104_CONTENT_TEXT_REV))) { // no length check, so longer than the UI // allows is made possible - if (equals(sub, F("settxt")) && // subcommand: settxt,, (stores + if (equals(sub, F("settxt")) && // subcommand: settxt,, (stores (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // the text in the settings, is not saved) it->text = string4; // Only if not too long, could 'blow up' the } // settings when saved @@ -1014,7 +1062,7 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, break; } - if (equals(sub, F("alignment")) && // subcommand: alignment,, (0..3) + if (equals(sub, F("alignment")) && // subcommand: alignment,, (0..3) (value4 >= 0) && (value4 <= static_cast(textPosition_t::PA_RIGHT))) { // last item in the enum it->alignment = value4; @@ -1160,10 +1208,10 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, # ifdef P104_USE_BAR_GRAPH - if ((equals(sub, F("bar")) || // subcommand: [set]bar,, (only allowed for zones - equals(sub, F("setbar"))) && // with Bargraph content) no length check, so longer than the UI + if ((equals(sub, F("bar")) || // subcommand: [set]bar,, (only allowed for zones + equals(sub, F("setbar"))) && // with Bargraph content) no length check, so longer than the UI (it->content == P104_CONTENT_BAR_GRAPH)) { // allows is made possible - if (equals(sub, F("setbar")) && // subcommand: setbar,, (stores the graph-string + if (equals(sub, F("setbar")) && // subcommand: setbar,, (stores the graph-string (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // in the settings, is not saved) it->text = string4; // Only if not too long, could 'blow up' the settings when saved } @@ -1173,6 +1221,14 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, } # endif // ifdef P104_USE_BAR_GRAPH + # ifdef P104_USE_DOT_SET + + if (equals(sub, F("dot"))) { // subcommand: dot,,,[,0][,,[,0]...] to draw + displayDots(zoneIndex - 1, *it, parseStringToEnd(string, 4)); // dots at row/column, add ,0 to turn a dot off + success = true; + } + # endif // ifdef P104_USE_DOT_SET + // FIXME TD-er: success is always false here. Maybe this must be done outside the for-loop? if (success) { // Reset the repeat timer if (it->repeatDelay > -1) { @@ -1705,8 +1761,8 @@ bool P104_data_struct::saveSettings() { if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, format( - F("P104: saveSettings zone: %d bufferSize: %d offset: %d"), - it->zone, bufferSize, saveOffset)); + F("P104: saveSettings zone: %d bufferSize: %d offset: %d"), + it->zone, bufferSize, saveOffset)); zbuffer.replace(P104_FIELD_SEP, P104_FIELD_DISP); addLog(LOG_LEVEL_INFO, zbuffer); } diff --git a/src/src/PluginStructs/P104_data_struct.h b/src/src/PluginStructs/P104_data_struct.h index 438d06159e..d71c5affdb 100644 --- a/src/src/PluginStructs/P104_data_struct.h +++ b/src/src/PluginStructs/P104_data_struct.h @@ -33,6 +33,7 @@ # define P104_USE_BAR_GRAPH // Enables the use of Bar-graph feature # define P104_USE_ZONE_ACTIONS // Enables the use of Actions per zone (New above/New below/Delete) # define P104_USE_ZONE_ORDERING // Enables the use of Zone ordering (Numeric order (1..n)/Display order (n..1)) +# define P104_USE_DOT_SET // Enables the use of Dot-set feature # define P104_ADD_SETTINGS_NOTES // Adds some notes on the Settings page @@ -326,11 +327,11 @@ struct P104_zone_struct { int8_t brightness = -1; int8_t inverted = 0; int8_t _lastChecked = -1; - # ifdef P104_USE_BAR_GRAPH + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) uint16_t _lower = 0u; uint16_t _upper = 0u; // lower and upper pixel numbers uint8_t _startModule = 0u; // starting module, end module is _startModule + size - 1 - # endif // ifdef P104_USE_BAR_GRAPH + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) }; # ifdef P104_USE_BAR_GRAPH @@ -338,12 +339,12 @@ struct P104_bargraph_struct { P104_bargraph_struct() = delete; // Not used, so leave out explicitly P104_bargraph_struct(uint8_t _graph) : graph(_graph) {} - ESPEASY_RULES_FLOAT_TYPE value{}; - ESPEASY_RULES_FLOAT_TYPE max{}; - ESPEASY_RULES_FLOAT_TYPE min{}; - uint8_t graph; - uint8_t barType = 0u; - uint8_t direction = 0u; + ESPEASY_RULES_FLOAT_TYPE value{}; + ESPEASY_RULES_FLOAT_TYPE max{}; + ESPEASY_RULES_FLOAT_TYPE min{}; + uint8_t graph; + uint8_t barType = 0u; + uint8_t direction = 0u; }; # endif // ifdef P104_USE_BAR_GRAPH @@ -384,14 +385,16 @@ struct P104_data_struct : public PluginTaskData_base { bool saveSettings(); void updateZone(uint8_t zone, const P104_zone_struct& zstruct); - # ifdef P104_USE_BAR_GRAPH + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) MD_MAX72XX *pM = nullptr; - void displayBarGraph(uint8_t zone, - const P104_zone_struct& zstruct, - const String & graph); void modulesOnOff(uint8_t start, uint8_t end, MD_MAX72XX::controlValue_t on_off); + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + # ifdef P104_USE_BAR_GRAPH + void displayBarGraph(uint8_t zone, + const P104_zone_struct& zstruct, + const String & graph); void drawOneBarGraph(uint16_t lower, uint16_t upper, int16_t pixBottom, @@ -402,6 +405,12 @@ struct P104_data_struct : public PluginTaskData_base { uint8_t row); # endif // ifdef P104_USE_BAR_GRAPH + # ifdef P104_USE_DOT_SET + void displayDots(uint8_t zone, + const P104_zone_struct& zstruct, + const String & dots); + # endif // ifdef P104_USE_DOT_SET + void displayOneZoneText(uint8_t currentZone, const P104_zone_struct& idx, const String & text); From af847f48c8f58cef0afb2eb011502719cf165489 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 13 Aug 2023 20:32:37 +0200 Subject: [PATCH 14/30] [P104] Update documentation --- docs/source/Plugin/P104.rst | 64 ++++++++++++++------------- docs/source/Plugin/P104_commands.repl | 17 +++++++ 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/source/Plugin/P104.rst b/docs/source/Plugin/P104.rst index c45b69a3fe..d4e36df2dc 100644 --- a/docs/source/Plugin/P104.rst +++ b/docs/source/Plugin/P104.rst @@ -42,7 +42,7 @@ This specific hardware setup can be somewhat confusing when initially setting up Zones ~~~~~~ -A Zone is a set of modules, electrically adjecent to each other, that can be configured to display a sinlge type of content. A zone consists of at least 1 and max. 255 modules. +A Zone is a set of modules, electrically adjacent to each other, that can be configured to display a single type of content. A zone consists of at least 1 and max. 255 modules. There is at least 1 zone, up to 8 zones can be configured on an ESP8266, and up to 16 zones on an ESP32. This limitation for ESP8266 is related to the amount of memory used by the libraries and the plugin. @@ -90,19 +90,19 @@ After adding the plugin to the Devices page, the configuration is shown: (Name w Task settings ~~~~~~~~~~~~~~ -* **Name** The name of the task. This should be unique for all devices that are configured. (Initially empty) +* **Name**: The name of the task. This should be unique for all devices that are configured. (Initially empty) -* **Enabled** For the device to work it has to be enabled. When checked, the device will be started as soon as the ESP starts. If desired, the device can also be enabled from f.e. a rule by using the ``TaskEnable,`` or ``TaskEnable,`` command, or disabled using the corresponding ``TaskDisable,|`` commands. +* **Enabled**: For the device to work it has to be enabled. When checked, the device will be started as soon as the ESP starts. If desired, the device can also be enabled from f.e. a rule by using the ``TaskEnable,`` or ``TaskEnable,`` command, or disabled using the corresponding ``TaskDisable,|`` commands. Actuator ~~~~~~~~ -* **GPIO -> CS** Select the GPIO pin that's wired to the ``CS`` input on the first MAX72xx module. +* **GPIO -> CS**: Select the GPIO pin that's wired to the ``CS`` input on the first MAX72xx module. Device settings ~~~~~~~~~~~~~~~~ -* **Hardware type** Selects the type of hardware. +* **Hardware type**: Selects the type of hardware. The list of available types is: @@ -117,45 +117,45 @@ All modules I ordered from Aliexpress so far work with the ``FC16`` hardware typ If multiple modules are to be connected daisy-chained, they all need to be of the same hardware type, as the Parola library that is used doesn't support different hardware types per zone. When multiple hardware types are to be used, then a separate Task (Devices tab) can be created matching those settings, and a separate GPIO pin should be configured for the ``CS`` pin. -* **Clear display on disable** When enabled, the display will be cleared when the task is disabled. +* **Clear display on disable**: When enabled, the display will be cleared when the task is disabled. -* **Log all displayed text (info)** When enabled, all content that is displayed, also when the Repeat option is used, are logged at Info level. The content will only be visible in the log if that is at Info level, or a more detailed level, configurable on the Tools/Advanced page. +* **Log all displayed text (info)**: When enabled, all content that is displayed, also when the Repeat option is used, are logged at Info level. The content will only be visible in the log if that is at Info level, or a more detailed level, configurable on the Tools/Advanced page. Content options ~~~~~~~~~~~~~~~~ The content options are global to all zones. -* **Clock with flashing colon** When enabled, the Clock content options will show a flashing colon between the Hour and Minute digits (1 second on, 1 second off). Enabled by default. +* **Clock with flashing colon**: When enabled, the Clock content options will show a flashing colon between the Hour and Minute digits (1 second on, 1 second off). Enabled by default. -* **Clock 12h display** When enabled, the Clock content options will use 12 hour display, instead of 24 hour. +* **Clock 12h display**: When enabled, the Clock content options will use 12 hour display, instead of 24 hour. -* **Clock 12h AM/PM display** When enabled, with also **Clock 12h display** enabled, will suffix the Clock content with ``a`` for AM and ``p`` for PM time. +* **Clock 12h AM/PM display**: When enabled, with also **Clock 12h display** enabled, will suffix the Clock content with ``a`` for AM and ``p`` for PM time. -* **Date format** Select the type of display for all Date and Date/time content types. +* **Date format**: Select the type of display for all Date and Date/time content types. Available options: .. image:: P104_DateFormatOptions.png :alt: Date format options -* **Date separator** Select the separator to use for Date content types. +* **Date separator**: Select the separator to use for Date content types. Available options: .. image:: P104_DateSeparatorOptions.png :alt: Date separator options -* **Year uses 4 digits** When enabled, the Date yr and Date/time content types use a 4 digit year display instead of 2 digits. +* **Year uses 4 digits**: When enabled, the Date yr and Date/time content types use a 4 digit year display instead of 2 digits. *(NB: On some challenged bin-size builds these Content options can be unavailable, then default settings will be used.)* Zones ~~~~~~ -* **Zones** Select the number of zones you want to configure. Adding or deleting a zone can also be done using the Actions column (when available). The maximum number of zones depends on the type of ESP used, on ESP8266 between 1 and 8 zones can be configured, and on ESP32, up to 16 zones can be set. When changing the number of zones, the page will be reloaded to reflect the new configuration. +* **Zones**: Select the number of zones you want to configure. Adding or deleting a zone can also be done using the Actions column (when available). The maximum number of zones depends on the type of ESP used, on ESP8266 between 1 and 8 zones can be configured, and on ESP32, up to 16 zones can be set. When changing the number of zones, the page will be reloaded to reflect the new configuration. -* **Zone order** As described above, the logical ordering of the modules is from bottom right to top left. For a more intuitive display of the zones, they can be shown from high to low instead of in numeric, low to high, order. The Actions column does take this setting into account when Add above or Add below is selected. When changing the Zone order, the page will be reloaded to show the zones in the new order. +* **Zone order**: As described above, the logical ordering of the modules is from bottom right to top left. For a more intuitive display of the zones, they can be shown from high to low instead of in numeric, low to high, order. The Actions column does take this setting into account when Add above or Add below is selected. When changing the Zone order, the page will be reloaded to show the zones in the new order. Available choices: @@ -167,11 +167,11 @@ Zone configuration For each Zone there are a number of settings that can be configured. -* **Modules** The number of modules that are in this zone. The maximum that can be set here is 255. +* **Modules**: The number of modules that are in this zone. The maximum that can be set here is 255. -* **Text** If a pre-defined text should be displayed for the zone, it can be set here. The usual variables and functions, as shown on the Tools/System Variables page can be used, as well as the ``[Task#]`` and ``[Var#]``, ``[Int#]`` and ``%v%`` variables. The system variables that produce special characters, like ``{D}`` etc., actually generate Unicode characters, and that is not supported by the, ASCII based, fonts used. To use any special characters look them up in the font tables, below, to have the desired characters displayed. This field is also used by the Bar graph content type for any predefined bar graph display. Maximum length of this field is 100 characters. If longer contents needs to be set, it can be set from the rules using the ``DotMatrix,,txt,""`` command. (``txt`` and ``settxt`` subcommands). +* **Text**: If a pre-defined text should be displayed for the zone, it can be set here. The usual variables and functions, as shown on the Tools/System Variables page can be used, as well as the ``[Task#]`` and ``[Var#]``, ``[Int#]`` and ``%v%`` variables. The system variables that produce special characters, like ``{D}`` etc., actually generate Unicode characters, and that is not supported by the, ASCII based, fonts used. To use any special characters look them up in the font tables, below, to have the desired characters displayed. This field is also used by the Bar graph content type for any predefined bar graph display. Maximum length of this field is 100 characters. If longer contents needs to be set, it can be set from the rules using the ``DotMatrix,,txt,""`` command. (``txt`` and ``settxt`` subcommands). -* **Content** Select the type of content shown in the zone. +* **Content**: Select the type of content shown in the zone. This are the available options: @@ -193,16 +193,16 @@ Content types can not be combined in a single zone. Content type: Bar graph: See below in the Commands section for a full description of the graph string options. -* **Alignment** The alignment of the content, except Bar graph, selected for the zone. +* **Alignment**: The alignment of the content, except Bar graph, selected for the zone. Available options: .. image:: P104_ZoneAlignmentOptions.png :alt: Zone alignment options -* **Animation In** The animation that is used to put the content into the zone. Default is Print, that just puts the content according to the alignment, in the zone. Animation option None is not available for **Animation In**, as the zone would not display anything. +* **Animation In**: The animation that is used to put the content into the zone. Default is Print, that just puts the content according to the alignment, in the zone. Animation option None is not available for **Animation In**, as the zone would not display anything. -* **Animation Out** The animation used to 'remove' the content from the zone. If set to None (the default), the content will stay in the zone until it is changed, or updated when the **Repeat (sec)** option is used. +* **Animation Out**: The animation used to 'remove' the content from the zone. If set to None (the default), the content will stay in the zone until it is changed, or updated when the **Repeat (sec)** option is used. Available options: @@ -216,11 +216,11 @@ The exact effect of each animation can best be seen by experimenting with them. Some animations are marked with an asterisk ``*`` as a warning that they should not be combined with a **Special Effects** setting that is *also* marked with an asterisk. The result is undefined, and may result in a distorted display. -* **Speed** The speed setting is used to control the speed factor of the animation in the zone in milliseconds. It is used for both the In and Out animations. When set to 0, the delay between each animation step is ~25 msec. +* **Speed**: The speed setting is used to control the speed factor of the animation in the zone in milliseconds. It is used for both the In and Out animations. When set to 0, the delay between each animation step is ~25 msec. -* **Pause** The delay after completing the **Animation In** before starting the **Animation Out**, in milliseconds. If no Out animation is set, this still adds to the time of completing the animation (before a next Repeat would be started). +* **Pause**: The delay after completing the **Animation In** before starting the **Animation Out**, in milliseconds. If no Out animation is set, this still adds to the time of completing the animation (before a next Repeat would be started). -* **Font** The font used to display the content. A few special fonts are available, allowing special characters to be displayed, and also a double-height font can be used to create a larger display where two similar zones are physically mounted above each other (as shown in the image in the Description section). When using a double-height font, also the **Layout** option should be set accordingly, and the **Content** setting should be the same, as well as any text. Bar graphs do not use a font, so when set for a zone, the **Font** and **Layout** settings are ignored. +* **Font**: The font used to display the content. A few special fonts are available, allowing special characters to be displayed, and also a double-height font can be used to create a larger display where two similar zones are physically mounted above each other (as shown in the image in the Description section). When using a double-height font, also the **Layout** option should be set accordingly, and the **Content** setting should be the same, as well as any text. Bar graphs do not use a font, so when set for a zone, the **Font** and **Layout** settings are ignored. Available options: @@ -247,7 +247,7 @@ Font overview: .. spacer -* **Layout** Determines wether the standard layout should be used, or the Upper/Lower part of a double-height font. +* **Layout**: Determines wether the standard layout should be used, or the Upper/Lower part of a double-height font. Available options: @@ -260,7 +260,7 @@ Available options: .. spacer -* **Inverted** With the Inverted option the text or bargraph on the display is either light on a dark background (Normal) or black on a light background (Inverted). +* **Inverted**: With the Inverted option the text or bargraph on the display is either light on a dark background (Normal) or black on a light background (Inverted). .. warning:: @@ -276,7 +276,7 @@ Available options: .. spacer -* **Special Effects** These effects can be used to turn a display 'upside down', in left/right 'mirror-image' or both. +* **Special Effects**: These effects can be used to turn a display 'upside down', in left/right 'mirror-image' or both. Available options: @@ -290,11 +290,11 @@ Available options: .. spacer -* **Offset** The offset can be used to skip a number of modules before a zone, f.e. when displaying a smaller zone on a larger set of displays, or when a module has a defect. A value between 0 and 254 kan be used here. +* **Offset**: The offset can be used to skip a number of modules before a zone, f.e. when displaying a smaller zone on a larger set of displays, or when a module has a defect. A value between 0 and 254 kan be used here. .. spacer -* **Brightness** Sets the brightness of the display, and thus highly influences the power required by a zone. Valid values are from 0 to 15, where 0 is very dimmed, and 15 is *very* bright (though that might differ per unit). +* **Brightness**: Sets the brightness of the display, and thus highly influences the power required by a zone. Valid values are from 0 to 15, where 0 is very dimmed, and 15 is *very* bright (though that might differ per unit). Measurements have been done to determine the current per pixel, when used at 5V: @@ -314,11 +314,11 @@ Measurements have been done to determine the current per pixel, when used at 5V: (Other brightness levels can be interpolated) -* **Repeat (sec)** Allows to repeat the current content of a zone to be redisplayed, using the animations as configured. Value is in seconds accuracy. The default value -1 indicates that repeat is disabled for the zone. +* **Repeat (sec)**: Allows to repeat the current content of a zone to be redisplayed, using the animations as configured. Value is in seconds accuracy. The default value -1 indicates that repeat is disabled for the zone. This will also repeat any content (usually *Text* or *Bar graph*) that is set using the corresponding commands (see below), or refresh a *Bar graph* on a regular interval. The initially set content is re-evaluated before it's displayed. -* **Action** These actions are related to changing the configuration of zones, not related to the display or content of a zones. +* **Action**: These actions are related to changing the configuration of zones, not related to the display or content of a zones. Available actions: @@ -347,4 +347,6 @@ Change log .. versionchanged:: 2.0 ... + |added| 2023-08-13 Add ``Dot`` subcommand. + |added| 2020-06-13 Initial version for ESPEasy. diff --git a/docs/source/Plugin/P104_commands.repl b/docs/source/Plugin/P104_commands.repl index f378bcfcdd..474c35b6ae 100644 --- a/docs/source/Plugin/P104_commands.repl +++ b/docs/source/Plugin/P104_commands.repl @@ -190,3 +190,20 @@ +-- Zero-point " + " + ``DotMatrix,Dot,,,[,0][,...]`` + + ``r`` Row coordinate, range 1..8. + + ``c`` Column coordinate, range 1..8 x module-count. + + ``0`` Optional zero value to turn the dot off. + "," + Draw individual dots on the display, in a row/column fashion, where an optional 0 can be added to turn a dot off, as the default is to turn a dot on. The row/column data doesn't have to be quoted. + + The row and colum have to stay within the confines of the zone. Each zone is 8 dots high, and a multiple of 8 dots wide, depending on the number of modules in the zone. + + While drawing the dots, the zone is paused, so there is no animation effect while drawing the dots. + + Dots can be drawn independent of the type of content set for the zone, but once the original content is being updated or redrawn, the dots will be overwritten. + " From ff18247d50eb339ca118b3a357c9242e352a64b1 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 13 Aug 2023 21:55:14 +0200 Subject: [PATCH 15/30] [P104] Code improvements and a bugfix --- src/_P104_max7219_Dotmatrix.ino | 6 +- src/src/PluginStructs/P104_data_struct.cpp | 108 ++++++++------------- 2 files changed, 40 insertions(+), 74 deletions(-) diff --git a/src/_P104_max7219_Dotmatrix.ino b/src/_P104_max7219_Dotmatrix.ino index 7019183dab..715c5833f2 100644 --- a/src/_P104_max7219_Dotmatrix.ino +++ b/src/_P104_max7219_Dotmatrix.ino @@ -252,11 +252,7 @@ boolean Plugin_104(uint8_t function, struct EventStruct *event, String& string) # ifdef P104_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - log.reserve(38); - log = F("dotmatrix: PLUGIN_INIT numDevices: "); - log += numDevices; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("dotmatrix: PLUGIN_INIT numDevices: "), numDevices)); } # endif // ifdef P104_DEBUG diff --git a/src/src/PluginStructs/P104_data_struct.cpp b/src/src/PluginStructs/P104_data_struct.cpp index 770a480411..e0d62328c5 100644 --- a/src/src/PluginStructs/P104_data_struct.cpp +++ b/src/src/PluginStructs/P104_data_struct.cpp @@ -137,17 +137,9 @@ void P104_data_struct::loadSettings() { } structDataSize = bufferSize; # ifdef P104_DEBUG_DEV - { - String log; - if (loglevelActiveFor(LOG_LEVEL_INFO) && - log.reserve(54)) { - log = F("P104: loadSettings stored Size: "); - log += structDataSize; - log += F(" taskindex: "); - log += taskIndex; - addLogMove(LOG_LEVEL_INFO, log); - } + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: loadSettings stored Size: %d taskindex: %d"), structDataSize, taskIndex)); } # endif // ifdef P104_DEBUG_DEV @@ -336,11 +328,7 @@ void P104_data_struct::loadSettings() { # ifdef P104_DEBUG_DEV if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P104: total zones initialized: "); - log += zoneIndex; - log += F(" expected: "); - log += expectedZones; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: total zones initialized: %d expected: %d"), zoneIndex, expectedZones)); } # endif // ifdef P104_DEBUG_DEV } @@ -448,13 +436,7 @@ void P104_data_struct::configureZones() { # ifdef P104_DEBUG_DEV if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P104: configureZones #"); - log += (currentZone + 1); - log += '/'; - log += expectedZones; - log += F(" offset: "); - log += zoneOffset; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: configureZones #%d/%d offset: %d"), currentZone + 1, expectedZones, zoneOffset)); } # endif // ifdef P104_DEBUG_DEV @@ -518,9 +500,7 @@ void P104_data_struct::displayOneZoneText(uint8_t zone, if (loglevelActiveFor(LOG_LEVEL_INFO) && logAllText && log.reserve(28 + text.length() + sZoneBuffers[zone].length())) { - log = F("dotmatrix: ZoneText: "); - log += zone + 1; // UI-number - log += F(", '"); + log = strformat(F("dotmatrix: ZoneText: %d, '"), zone + 1); // UI-number log += text; log += F("' -> '"); log += sZoneBuffers[zone]; @@ -656,7 +636,7 @@ void P104_data_struct::displayBarGraph(uint8_t zone, sZoneInitial[zone] = graph; // Keep the original string for future use # define NOT_A_COMMA 0x02 // Something else than a comma, or the parseString function will get confused - String parsedGraph = graph; // Extra copy created so we don't mess up the incoming String + String parsedGraph(graph); // Extra copy created so we don't mess up the incoming String parsedGraph = parseTemplate(parsedGraph); parsedGraph.replace(',', NOT_A_COMMA); @@ -877,7 +857,9 @@ void P104_data_struct::displayDots(uint8_t zone, } pM->setPoint(row - 1, zstruct._upper - (col - 1), on_off); // Reverse layout } - idx += 2; // Skip to next argument set + idx += 2; // Skip to next argument set + + if (idx % 16 == 0) { delay(0); } sRow = parseString(dots, idx + 1); sCol = parseString(dots, idx + 2); sOn_off = parseString(dots, idx + 3); @@ -979,14 +961,14 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, # ifdef P104_USE_COMMANDS bool reconfigure = false; # endif // ifdef P104_USE_COMMANDS - bool success = false; - String command = parseString(string, 1); + bool success = false; + const String command = parseString(string, 1); if ((nullptr != P) && equals(command, F("dotmatrix"))) { // main command: dotmatrix - String sub = parseString(string, 2); + const String sub = parseString(string, 2); int zoneIndex; - String string4 = parseStringKeepCaseNoTrim(string, 4); + const String string4 = parseStringKeepCaseNoTrim(string, 4); # ifdef P104_USE_COMMANDS int value4; validIntFromString(string4, value4); @@ -1010,7 +992,7 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, // Zone-specific subcommands if (validIntFromString(parseString(string, 3), zoneIndex) && (zoneIndex > 0) && - (static_cast(zoneIndex) <= zones.size())) { + (static_cast(zoneIndex) <= zones.size())) { // subcommands are processed in the same order as they are presented in the UI for (auto it = zones.begin(); it != zones.end() && !success; ++it) { if ((it->zone == zoneIndex)) { // This zone @@ -1176,12 +1158,12 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, (value4 >= 0) && (value4 <= P104_BRIGHTNESS_MAX)) { it->brightness = value4; - P->setIntensity(zoneIndex - 1, it->brightness); // Change brightness directly + P->setIntensity(zoneIndex - 1, it->brightness); // Change brightness immediately success = true; break; } - if (equals(sub, F("repeat")) && // subcommand: repeaat,, (-1..86400 = 24h) + if (equals(sub, F("repeat")) && // subcommand: repeat,, (-1..86400 = 24h) (value4 >= -1) && (value4 <= P104_MAX_REPEATDELAY_VALUE)) { it->repeatDelay = value4; @@ -1192,18 +1174,6 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, } break; } - # else // ifdef P104_USE_COMMANDS - { - String validCommands = F( - "|size|content|alignment|anim.in|speed|anim.out|pause|font|inverted|layout|specialeffect|offset|brightness|repeat|"); - String testSub = '|'; - testSub += sub; - testSub += '|'; - - if (validCommands.indexOf(testSub) > -1) { - addLog(LOG_LEVEL_ERROR, F("dotmatrix: subcommand not included in build.")); - } - } # endif // ifdef P104_USE_COMMANDS # ifdef P104_USE_BAR_GRAPH @@ -1226,6 +1196,7 @@ bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, if (equals(sub, F("dot"))) { // subcommand: dot,,,[,0][,,[,0]...] to draw displayDots(zoneIndex - 1, *it, parseStringToEnd(string, 4)); // dots at row/column, add ,0 to turn a dot off success = true; + break; } # endif // ifdef P104_USE_DOT_SET @@ -1760,9 +1731,8 @@ bool P104_data_struct::saveSettings() { # ifdef P104_DEBUG_DEV if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, format( - F("P104: saveSettings zone: %d bufferSize: %d offset: %d"), - it->zone, bufferSize, saveOffset)); + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: saveSettings zone: %d bufferSize: %d offset: %d"), + it->zone, bufferSize, saveOffset)); zbuffer.replace(P104_FIELD_SEP, P104_FIELD_DISP); addLog(LOG_LEVEL_INFO, zbuffer); } @@ -1830,11 +1800,11 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { } { - addFormCheckBox(F("Clear display on disable"), F("cleardisable"), + addFormCheckBox(F("Clear display on disable"), F("clrdsp"), bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE)); addFormCheckBox(F("Log all displayed text (info)"), - F("logalltext"), + F("logtxt"), bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT)); } @@ -1842,9 +1812,9 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { { addFormSubHeader(F("Content options")); - addFormCheckBox(F("Clock with flashing colon"), F("clockflash"), !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH)); - addFormCheckBox(F("Clock 12h display"), F("clock12h"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H)); - addFormCheckBox(F("Clock 12h AM/PM indicator"), F("clockampm"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM)); + addFormCheckBox(F("Clock with flashing colon"), F("clkflash"), !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH)); + addFormCheckBox(F("Clock 12h display"), F("clk12h"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H)); + addFormCheckBox(F("Clock 12h AM/PM indicator"), F("clkampm"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM)); } { // Date format const __FlashStringHelper *dateFormats[] = { @@ -1857,7 +1827,7 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { P104_DATE_FORMAT_US, P104_DATE_FORMAT_JP }; - addFormSelector(F("Date format"), F("dateformat"), + addFormSelector(F("Date format"), F("datefmt"), 3, dateFormats, dateFormatOptions, get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT)); @@ -1875,7 +1845,7 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { P104_DATE_SEPARATOR_DASH, P104_DATE_SEPARATOR_DOT }; - addFormSelector(F("Date separator"), F("dateseparator"), + addFormSelector(F("Date separator"), F("datesep"), 4, dateSeparators, dateSeparatorOptions, get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR)); @@ -1907,7 +1877,7 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { zonetip += F(" will save and reload the page."); } # endif // if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES) - addFormSelector(F("Zones"), F("zonecount"), P104_MAX_ZONES, zonesList, zonesOptions, nullptr, P104_CONFIG_ZONE_COUNT, true + addFormSelector(F("Zones"), F("zonecnt"), P104_MAX_ZONES, zonesList, zonesOptions, nullptr, P104_CONFIG_ZONE_COUNT, true # ifdef P104_USE_TOOLTIPS , zonetip # endif // ifdef P104_USE_TOOLTIPS @@ -2177,9 +2147,9 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { F("Clock sec (6 mod)"), F("Date (4 mod)"), F("Date yr (6/7 mod)"), - F("Date/time (9/13 mod)") + F("Date/time (9/13 mod)"), # ifdef P104_USE_BAR_GRAPH - , F("Bar graph") + F("Bar graph"), # endif // ifdef P104_USE_BAR_GRAPH }; const int contentOptions[] { @@ -2189,9 +2159,9 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { P104_CONTENT_TIME_SEC, P104_CONTENT_DATE4, P104_CONTENT_DATE6, - P104_CONTENT_DATE_TIME + P104_CONTENT_DATE_TIME, # ifdef P104_USE_BAR_GRAPH - , P104_CONTENT_BAR_GRAPH + P104_CONTENT_BAR_GRAPH, # endif // ifdef P104_USE_BAR_GRAPH }; const __FlashStringHelper *invertedTypes[3] = { @@ -2499,11 +2469,11 @@ bool P104_data_struct::webform_load(struct EventStruct *event) { * webform_save **************************************************************/ bool P104_data_struct::webform_save(struct EventStruct *event) { - P104_CONFIG_ZONE_COUNT = getFormItemInt(F("zonecount")); + P104_CONFIG_ZONE_COUNT = getFormItemInt(F("zonecnt")); P104_CONFIG_HARDWARETYPE = getFormItemInt(F("hardware")); - bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE, isFormItemChecked(F("cleardisable"))); - bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT, isFormItemChecked(F("logalltext"))); + bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE, isFormItemChecked(F("clrdsp"))); + bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT, isFormItemChecked(F("logtxt"))); # ifdef P104_USE_ZONE_ORDERING zoneOrder = getFormItemInt(F("zoneorder")); // Is used in saveSettings() @@ -2512,12 +2482,12 @@ bool P104_data_struct::webform_save(struct EventStruct *event) { # ifdef P104_USE_DATETIME_OPTIONS uint32_t ulDateTime = 0; - bitWrite(ulDateTime, P104_CONFIG_DATETIME_FLASH, !isFormItemChecked(F("clockflash"))); // Inverted flag - bitWrite(ulDateTime, P104_CONFIG_DATETIME_12H, isFormItemChecked(F("clock12h"))); - bitWrite(ulDateTime, P104_CONFIG_DATETIME_AMPM, isFormItemChecked(F("clockampm"))); + bitWrite(ulDateTime, P104_CONFIG_DATETIME_FLASH, !isFormItemChecked(F("clkflash"))); // Inverted flag + bitWrite(ulDateTime, P104_CONFIG_DATETIME_12H, isFormItemChecked(F("clk12h"))); + bitWrite(ulDateTime, P104_CONFIG_DATETIME_AMPM, isFormItemChecked(F("clkampm"))); bitWrite(ulDateTime, P104_CONFIG_DATETIME_YEAR4DGT, isFormItemChecked(F("year4dgt"))); - set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_FORMAT, getFormItemInt(F("dateformat"))); - set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_SEP_CHAR, getFormItemInt(F("dateseparator"))); + set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_FORMAT, getFormItemInt(F("datefmt"))); + set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_SEP_CHAR, getFormItemInt(F("datesep"))); P104_CONFIG_DATETIME = ulDateTime; # endif // ifdef P104_USE_DATETIME_OPTIONS From f8b671773697265baa81a6d4373ea85ed638bc23 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 18 Aug 2023 19:17:06 +0200 Subject: [PATCH 16/30] [P028] Fix merge conflict --- src/src/WebServer/ControllerPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/WebServer/ControllerPage.cpp b/src/src/WebServer/ControllerPage.cpp index 29bf063f1f..bb511a95cd 100644 --- a/src/src/WebServer/ControllerPage.cpp +++ b/src/src/WebServer/ControllerPage.cpp @@ -259,7 +259,7 @@ void handle_controllers_ShowAllControllersTable() html_TD(); if ((INVALID_PROTOCOL_INDEX == ProtocolIndex) || (Protocol[ProtocolIndex].usesPort)) { - addHtmlInt(13 == Settings.Protocol[x] ? Settings.UDPPort : ControllerSettings.Port); // P2P/C013 exception + addHtmlInt(13 == Settings.Protocol[x] ? Settings.UDPPort : ControllerSettings->Port); // P2P/C013 exception } } else { From b3ccac8b1f9968d8db352b9984efc13f5531022b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 22 Aug 2023 21:13:28 +0200 Subject: [PATCH 17/30] [Rules] Improving removal of comments --- src/src/Helpers/RulesHelper.cpp | 53 ++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/src/Helpers/RulesHelper.cpp b/src/src/Helpers/RulesHelper.cpp index 1c95b00700..01cb9466fa 100644 --- a/src/src/Helpers/RulesHelper.cpp +++ b/src/src/Helpers/RulesHelper.cpp @@ -61,6 +61,57 @@ bool rules_strip_trailing_comments(String& line) // Strip trailing comments int comment = line.indexOf(F("//")); + if (comment >= 0) { + uint8_t quotes = 0u; + uint8_t qindex = 0u; + bool firstSlash = false; + bool haveColon = false; + comment = -1; // No comment confirmed yet + + // Find first comment '//' that's not quoted or prefixed with a colon '://' + for (size_t i = 0; i < line.length() && comment == -1; i++) { + switch (line[i]) { + case ':': + haveColon = true; + break; + case '/': + + if (!quotes && !haveColon) { + if (firstSlash) { + comment = i - 1u; // Found a valid comment-start + } else { + firstSlash = true; + } + } else { + firstSlash = false; + haveColon = false; + } + break; + case '\"': // bit 2 + qindex++; + case '\'': // bit 1 + qindex++; + case '`': // bit 0 + + if (bitRead(quotes, qindex)) { + bitClear(quotes, qindex); + } else { + bitSet(quotes, qindex); + } + firstSlash = false; + haveColon = false; + qindex = 0; + break; + default: + firstSlash = false; + haveColon = false; + break; + } + } + + // We could report an 'unbalanced quotes error' if quotes != 0, but it would only be logged if the line contains a comment... + } + if (comment >= 0) { line = line.substring(0, comment); line.trim(); @@ -220,7 +271,7 @@ bool RulesHelperClass::addChar(char c, String& line, bool& firstNonSpaceRead, if (!commentFound) { line += '/'; - if (line.endsWith(F("//"))) { + if (line.endsWith(F("//")) && !line.endsWith(F("://"))) { // consider the rest of the line a comment commentFound = true; } From f9b66e925835fdd10e3dff875324c3227bec9c71 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 23 Aug 2023 10:42:58 +0200 Subject: [PATCH 18/30] [Rules] Separate comment removal from reading a line --- src/src/Helpers/RulesHelper.cpp | 68 +++++++++++---------------------- src/src/Helpers/RulesHelper.h | 3 +- 2 files changed, 23 insertions(+), 48 deletions(-) diff --git a/src/src/Helpers/RulesHelper.cpp b/src/src/Helpers/RulesHelper.cpp index 01cb9466fa..33ba11d68f 100644 --- a/src/src/Helpers/RulesHelper.cpp +++ b/src/src/Helpers/RulesHelper.cpp @@ -62,10 +62,9 @@ bool rules_strip_trailing_comments(String& line) int comment = line.indexOf(F("//")); if (comment >= 0) { - uint8_t quotes = 0u; - uint8_t qindex = 0u; - bool firstSlash = false; - bool haveColon = false; + bool firstSlash = false; + bool haveColon = false; + char wrapQuote = '\0'; comment = -1; // No comment confirmed yet // Find first comment '//' that's not quoted or prefixed with a colon '://' @@ -76,7 +75,7 @@ bool rules_strip_trailing_comments(String& line) break; case '/': - if (!quotes && !haveColon) { + if ((wrapQuote == '\0') && !haveColon) { if (firstSlash) { comment = i - 1u; // Found a valid comment-start } else { @@ -87,29 +86,26 @@ bool rules_strip_trailing_comments(String& line) haveColon = false; } break; - case '\"': // bit 2 - qindex++; - case '\'': // bit 1 - qindex++; - case '`': // bit 0 - - if (bitRead(quotes, qindex)) { - bitClear(quotes, qindex); - } else { - bitSet(quotes, qindex); + case '"': + case '\'': + case '`': + + if ((wrapQuote == '\0') && (i > 0) && ((line[i - 1] == ' ') || (line[i - 1] == ','))) { // Start-quote? + wrapQuote = line[i]; // Start quoted range + } else if ((line[i] == wrapQuote) && // End-quote equals start-quote? + // And next is a separator: ' ,/' + (((i < line.length() - 1) && ((line[i + 1] == ' ') || (line[i + 1] == ',') || (line[i + 1] == '/'))) || + (i == line.length() - 1))) { // Or end of line? + wrapQuote = '\0'; // No longer in quoted range } - firstSlash = false; - haveColon = false; - qindex = 0; - break; + + // Fall through default: firstSlash = false; haveColon = false; break; } } - - // We could report an 'unbalanced quotes error' if quotes != 0, but it would only be logged if the line contains a comment... } if (comment >= 0) { @@ -232,7 +228,7 @@ size_t RulesHelperClass::read(const String& filename, size_t& pos, uint8_t *buff #endif // ifndef CACHE_RULES_IN_MEMORY -bool RulesHelperClass::addChar(char c, String& line, bool& firstNonSpaceRead, bool& commentFound) +bool RulesHelperClass::addChar(char c, String& line, bool& firstNonSpaceRead) { switch (c) { @@ -242,9 +238,7 @@ bool RulesHelperClass::addChar(char c, String& line, bool& firstNonSpaceRead, line.trim(); if ((line.length() > 0) && !line.startsWith(F("//"))) { - if (commentFound) { - rules_strip_trailing_comments(line); - } + rules_strip_trailing_comments(line); check_rules_line_user_errors(line); return true; } @@ -252,7 +246,6 @@ bool RulesHelperClass::addChar(char c, String& line, bool& firstNonSpaceRead, // Prepare for new line line.clear(); firstNonSpaceRead = false; - commentFound = false; break; } case '\r': // Just skip this character @@ -266,25 +259,11 @@ bool RulesHelperClass::addChar(char c, String& line, bool& firstNonSpaceRead, } break; } - case '/': - { - if (!commentFound) { - line += '/'; - - if (line.endsWith(F("//")) && !line.endsWith(F("://"))) { - // consider the rest of the line a comment - commentFound = true; - } - } - break; - } default: // Any other character { firstNonSpaceRead = true; - if (!commentFound) { - line += c; - } + line += c; break; } } @@ -308,18 +287,16 @@ String RulesHelperClass::readLn(const String& filename, RulesLines lines; String tmpStr; bool firstNonSpaceRead = false; - bool commentFound = false; // Keep track of which line we're reading for the event cache. size_t readPos = 0; while (f.available()) { - if (addChar(char(f.read()), tmpStr, firstNonSpaceRead, commentFound)) { + if (addChar(char(f.read()), tmpStr, firstNonSpaceRead)) { lines.push_back(tmpStr); ++readPos; firstNonSpaceRead = false; - commentFound = false; tmpStr.clear(); } } @@ -371,7 +348,6 @@ String RulesHelperClass::readLn(const String& filename, buf.resize(RULES_BUFFER_SIZE); bool firstNonSpaceRead = false; - bool commentFound = false; // Try to get the best possible estimate on line length based on earlier parsing of the rules. static size_t longestLineSize = RULES_BUFFER_SIZE; @@ -391,7 +367,7 @@ String RulesHelperClass::readLn(const String& filename, for (int x = 0; x < len; x++) { int data = buf[x]; - if (addChar(char(data), line, firstNonSpaceRead, commentFound)) { + if (addChar(char(data), line, firstNonSpaceRead)) { if (line.length() > longestLineSize) { longestLineSize = line.length(); } diff --git a/src/src/Helpers/RulesHelper.h b/src/src/Helpers/RulesHelper.h index 640f43f17a..2089bb5ffc 100644 --- a/src/src/Helpers/RulesHelper.h +++ b/src/src/Helpers/RulesHelper.h @@ -52,8 +52,7 @@ class RulesHelperClass { bool addChar(char c, String& line, - bool & firstNonSpaceRead, - bool & commentFound); + bool & firstNonSpaceRead); public: From dccd13f0f198c5cbe4c6f1237a269472a45fdfc5 Mon Sep 17 00:00:00 2001 From: uwekaditz Date: Sun, 27 Aug 2023 16:13:14 +0200 Subject: [PATCH 19/30] 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 20/30] 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 21/30] 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 22/30] 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 From 4d2f4d7f7d87342a77b0584a7309e302eb310b16 Mon Sep 17 00:00:00 2001 From: chromoxdor <33860956+chromoxdor@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:40:56 +0200 Subject: [PATCH 23/30] Fixed error in section TaskValueSet --- docs/source/Rules/Rules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Rules/Rules.rst b/docs/source/Rules/Rules.rst index 7509f01408..bde0e51fdc 100644 --- a/docs/source/Rules/Rules.rst +++ b/docs/source/Rules/Rules.rst @@ -1521,7 +1521,7 @@ Just create Generic - Dummy Device and variables inside it. Alternatively, TASKname and/or VARname can be used instead of TASKnr and VARnr: - .. code-block:: html +.. code-block:: none TaskValueSet,TASKname,VARname,Value TaskValueSet,TASKnr,VARname,Value From 1889fcc0e39d20f3d1ceeb3b6e2c50fc9f52bc1c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 5 Sep 2023 21:50:55 +0200 Subject: [PATCH 24/30] [P051] Fix device being automatically disabled and I2C scan issue --- src/_P051_AM2320.ino | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/_P051_AM2320.ino b/src/_P051_AM2320.ino index 0452a7377e..81c671acbe 100644 --- a/src/_P051_AM2320.ino +++ b/src/_P051_AM2320.ino @@ -12,14 +12,19 @@ // of the above library // +/** Changelog: + * 2023-09-05 tonhuisman: Disable I2C device-check during read, as the sensor seems a bit 'itchy' about that + * 2023-09-05 tonhuisman: Add changelog + */ -#include -#define PLUGIN_051 -#define PLUGIN_ID_051 51 -#define PLUGIN_NAME_051 "Environment - AM2320" -#define PLUGIN_VALUENAME1_051 "Temperature" -#define PLUGIN_VALUENAME2_051 "Humidity" +# include + +# define PLUGIN_051 +# define PLUGIN_ID_051 51 +# define PLUGIN_NAME_051 "Environment - AM2320" +# define PLUGIN_VALUENAME1_051 "Temperature" +# define PLUGIN_VALUENAME2_051 "Humidity" boolean Plugin_051(uint8_t function, struct EventStruct *event, String& string) @@ -42,6 +47,7 @@ boolean Plugin_051(uint8_t function, struct EventStruct *event, String& string) Device[deviceCount].TimerOption = true; Device[deviceCount].GlobalSyncOption = true; Device[deviceCount].PluginStats = true; + Device[deviceCount].I2CNoDeviceCheck = true; // Avoid device check break; } From 725fb8f4069a3353576f6278b7f2073326cb641d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 5 Sep 2023 22:00:21 +0200 Subject: [PATCH 25/30] [I2C Scan] Make I2C scan more robust by adding extra wakeup --- src/src/WebServer/I2C_Scanner.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/src/WebServer/I2C_Scanner.cpp b/src/src/WebServer/I2C_Scanner.cpp index 9a74fdb983..5b4b6bf166 100644 --- a/src/src/WebServer/I2C_Scanner.cpp +++ b/src/src/WebServer/I2C_Scanner.cpp @@ -10,6 +10,7 @@ #include "../Helpers/_Plugin_init.h" #include "../Helpers/Hardware.h" +#include "../Helpers/I2C_access.h" #include "../Helpers/StringConverter.h" @@ -40,8 +41,9 @@ int scanI2CbusForDevices_json( // Utility function for scanning the I2C bus for } if (!skipCheck) { // Ignore I2C multiplexer and addresses to exclude when scanning its channels #endif // if FEATURE_I2CMULTIPLEXER - Wire.beginTransmission(address); - error = Wire.endTransmission(); + I2C_wakeup(address); // Wakeup, workaround for slow-responding devices, see https://github.com/letscontrolit/ESPEasy/issues/3781 + delay(1); + error = I2C_wakeup(address); // Get status delay(1); if ((error == 0) || (error == 4)) @@ -370,8 +372,9 @@ int scanI2CbusForDevices( // Utility function for scanning the I2C bus for valid } if (!skipCheck) { // Ignore I2C multiplexer and addresses to exclude when scanning its channels #endif // if FEATURE_I2CMULTIPLEXER - Wire.beginTransmission(address); - error = Wire.endTransmission(); + I2C_wakeup(address); // Wakeup, workaround for slow-responding devices, see https://github.com/letscontrolit/ESPEasy/issues/3781 + delay(1); + error = I2C_wakeup(address); // Get status delay(1); switch (error) { From 8a7ac4230eda599885b0a60c7a3ad44b56089397 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 5 Sep 2023 22:00:49 +0200 Subject: [PATCH 26/30] [Docs] Add I2C Scan documentation --- docs/source/Tools/Tools.rst | 18 ++++++++++++++++++ .../Tools/images/Tools_I2Cscan_single_bus.png | Bin 0 -> 5006 bytes 2 files changed, 18 insertions(+) create mode 100644 docs/source/Tools/images/Tools_I2Cscan_single_bus.png diff --git a/docs/source/Tools/Tools.rst b/docs/source/Tools/Tools.rst index e339cba996..748a07da2b 100644 --- a/docs/source/Tools/Tools.rst +++ b/docs/source/Tools/Tools.rst @@ -877,6 +877,24 @@ System Variables ================ +I2C Scan +======== + +To verify if any connected I2C devices are properly detected by the ESP, the I2C Scan is available. This will scan the I2C bus, and, when configured, the additional busses provided via an I2C multiplexer, for available devices. + +The scan is performed if the I2C ``SDA`` and ``SCL`` GPIO pins are configured on the Hardware page, and will use the configured ``Slow device Clock Speed`` setting (default: 100 kHz) during the scan, as that should be supported by any I2C device available. + +The output is a list of all addresses, in hexadecimal notation, and, when included in the build, the known device name(s) supported at that address. On the same condition, and when the plugin for the detected device is included in the build, the name of the plugin is also listed: + +Example scan showing a single device, with the Plugin included in the build: + +.. image:: images/Tools_I2Cscan_single_bus.png + +TODO: *Add scan with I2C Multiplexer result* + +.. note:: On builds that have ``LIMIT_BUILD_SIZE`` set, like the ESP8266 Collection and Display builds, the names of the supported devices and plugins are not included in the output, only the address(es) are listed. + + Factory Reset ============= diff --git a/docs/source/Tools/images/Tools_I2Cscan_single_bus.png b/docs/source/Tools/images/Tools_I2Cscan_single_bus.png new file mode 100644 index 0000000000000000000000000000000000000000..69c0c134336fdf52f4dd2b5ede0284bbec10cb47 GIT binary patch literal 5006 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 U1^vM2dvOdsV}F`>>T2x&0O*R&;Q#;t literal 0 HcmV?d00001 From 87a28bd4f80237c728f0ded0c79a88f256901610 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 9 Sep 2023 13:13:54 +0200 Subject: [PATCH 27/30] [Docs] Additional I2C Scan documentation (mux) --- docs/source/Tools/Tools.rst | 7 +++++-- .../Tools/images/Tools_I2Cscan_multiplexer.png | Bin 0 -> 58741 bytes 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 docs/source/Tools/images/Tools_I2Cscan_multiplexer.png diff --git a/docs/source/Tools/Tools.rst b/docs/source/Tools/Tools.rst index 748a07da2b..f934c154f9 100644 --- a/docs/source/Tools/Tools.rst +++ b/docs/source/Tools/Tools.rst @@ -890,9 +890,12 @@ Example scan showing a single device, with the Plugin included in the build: .. image:: images/Tools_I2Cscan_single_bus.png -TODO: *Add scan with I2C Multiplexer result* +Example scan using an I2C multiplexer, showing multiple devices across multiple channels, with the plugins included in the (MAX) build: -.. note:: On builds that have ``LIMIT_BUILD_SIZE`` set, like the ESP8266 Collection and Display builds, the names of the supported devices and plugins are not included in the output, only the address(es) are listed. +.. image:: images/Tools_I2Cscan_multiplexer.png + + +.. note:: On builds that have ``LIMIT_BUILD_SIZE`` set, like the ESP8266 Collection and Display builds, the names of the supported devices and plugins are **not** included in the output, only the address(es) are listed. Factory Reset diff --git a/docs/source/Tools/images/Tools_I2Cscan_multiplexer.png b/docs/source/Tools/images/Tools_I2Cscan_multiplexer.png new file mode 100644 index 0000000000000000000000000000000000000000..b23bd501eecd1648f2353d9b8e109df2587ae2a6 GIT binary patch literal 58741 zcmce;cU+U*wl0c@C4TgX(gc(uy%#~6N|h=_IuSyLfPi#Dqz_#NR<|(Mn&lm zkP;vir3DE^2oORjH?F<*S+e%|1Q~pr8t;prAaue3AT2faTjI^20@Y9ZfazBl#jeXoZm<08h0nd?_eszx@6_mo7xh zMt)N5nY#HiHAi2tkL%NCu3nxLU>6@Rh$GDvad{E)m!!C)6qhiUgoJ{mtb+I*nk(wY zhEkFu65`TaB3znYUd|qlTo3;7j)I$!8x#~=6xwR{Oag7!^OaH-6~9%5c!L|zf0bVt zyTVuGKg@QZU_XFz4_KJ}WbSpBps;9d&~$K~u}2Z0%4o`4Q~=z5_AHs}M2k0DGii4t zg8u&Hn^FI`MWf|f&;9df@)Pm5b-DiQ59D87ixqmEl8~n%=YFr1d_Auv${ynrVMpgF zDD-K!*(sjrzSyFmcuJL}NRrYBf}HQa}?B6|KSkuZsdt?_TD@Ubdn7@#Uf8zJq!*}$fMfw8SAl3 zp#v;ZtpCwz`wpRhM|3vb=* zHJuqg392Qu-fzsEACx=y+*egmh3zxd%ky@wRDUn??R~l9&b7y0{~W;c{e+S(`hfPD zGX*aVy)Qxf9PsjY7xD1#_@{hHmtR<2ti80Mv?Ty6QslgHe(iq^HAl76)yu)w6!XL5 zt>O0byc#YyX6X>>6V$RwZs{oDeLR5b*Eaq9Yqc4+FTQvS77P{Tf%WfE-}4O@h&Zey zXeX853l*@U`9|D66;$y|<}3xWL%E-Nb)~Ls{ zV%@s|q(e~sLwvheHQa*w#K5N@?VJqBDita(>;1mmLr*85kIEjAJbgzd~dbT^8$$t;`r$4T0fRGG@&V$%= zxegTf`G0z`B6M3^ciD|QFX|UxeG$U>(I!dn*5v2g_x`aYqsd8bj-8s4`6qOXGjBvR zrZ1UsJ5w^ZoxbzX*5wWb~^4WUve{Ka0b89)u2Uk*B} zDF%4yd-NU7xRE&1@ee#HS+Z1r1(k7GBwctA5_u)jHR;lU2(y#iNTcm+0?p(t2H$%e z-@(6i69H&Y86M~0sQL1>T&X~P%XSk}KbdFmaLwwiYq2huqe)rJ%CAqByUoP|who-7 zvreZ8DP@$8oKylx2&IviIE76Pp1vN)(~B>*z9)749V>E6R&V{s=Ui36gb=(%HqbQ} z7A=;*L`YaDNJ^gtH@>t9%XQ~otGDAshCSelYxT@U){tr^1FJD&sAPy{BjofX=7Sgo zS<#&rI3|337Pb+`bRK(fL-4;LUPy3NUHU^yZSi6WNN*&L;WifvT`SddFrTAl`a3dnY}02 z&N6rFU()pRH$QhpXj>^>woiyRP}n^1+26VV{&0yvi@r{3azW>ATk6Pj-gayaf}5N+ zZIpCWD^GS*klMCN*=m;i5UDfJbKg11BF|nQsoqKA$dcL2(nnCR&ZbpMg@2A(m^e8% z1K2+DD2wU*`{-VEDF4;1T^n`!CfRoZ0eS<{wdUOe9wZySVcUruDQ%+SSif{v#~uv6 zXp|mc^mWpqj;GIJH+1ePof{90BjY6$?e>kFrJA7Ymm)s6>u~|EukZI%@*`qUxRrwI zyMyC$5Zp^=LH(Ehuax!F_>!9E{r7%wE*~sgPljo1IPW56Tf``i&I2zi16i)(FL`ah zw`sO`1Md88ad(bK;h64w$1#u1#qYJhMI0tR7*EZlv)&oUryTCK~0y3!a)v%ig5xwq`gQ3~JT$uI9{jYnFqTzdfCum(ght zXTu^zQ>K8IF!4b!^ODbXXlsz;xo>lq>t#cYR?dHD1x)XsRg4%2rr;AUnGjyB@ zQ4y*IDbn9(_2Yn`~Oq8qJ& z3$I6%Je=0Ol3TJ`3g=vjaS*C0%G_~St52osJ2)1rliI%Y3a-Ums~0`(bx zI!rmLeCWI`*T{(tf$MI@v<#)eU)wjXDqk#{v|lKCfg+ZtigeY$`tQKewI>kLL|o%| zp!vSM^nDXWq3J<5(e!Sktl!MW+5^7}`i5R8==)*_Oz#DWGSpR^43#cM1kzH7^L+^j zB5dM4Wc(jvdGgZ2z&P-N8QdR}w>{|sz6i#$BYAM(#DZm{VmTm^PQ zLGc?coTrEbY~P@uNT2{-rg*~j0!u*wz8`vt;yL$6DoP6G%M0=pAG7G^&ry88d}gH- z6n7|fxhS58bN$~PVhfAzZ|WyBArYrAQb}O*5o7nJE#O>_Q zCU|3)GeuO!bJK%}%WG-AgyW4JbOVf|bW&w>bpB%cVR7xjMiU;5AwVnP1ECy69Y=fq z#DcDllldXD*cRDLcTE($SzlLWp<|rJ?qS2x<4fyb~`mFiTCZJ6ouPF z%`7I)aF$djz4xZy+E@icBs+Lkd-?IOt-W-NWo0aKt+R0edJXF=1E|KK_wv3`)sc$i zP4@Z~)``|NwJSKGY$ZnwV*Q(^C)E^g7(s`Q_!IffM}ipafvCLc4r~#^s8|@tJAgMw zLtmY9dTjTBNjFT?>fp@@t=mNn$o#!f8ep0AG8mWSE8f(E{z)pK;hIJ5u;N-pf3W^3 zf5KgQK(T?5lwj<;qk~t%a72xE&1z@L%%Jj&y0(8a(%ICdZ~}Sx1ZVgt$6tQ z6{c64^!Q^fxI54{^3i^X)gx&;(50s|f7&l8zD#Z3 zK!^cF!;m|ctMJVqlc3k&3+&mfS0f3PJn-;bimm^eE8oH4Wq)UV(yYPN@Ak+{ zEP$Hptnk%%>tL*aXUS0$x@n5FB7?WgkW%4-D?M$XhV&FSJIb#Be|MP0(CxV!fM(%Q z%x1cD3e54t5_S%nH<%++jg$fQShL}4?Pr>U(5t61AKZm+f7o;mCKaw3Vw~25y829= zA=4L&vYb~h{^=T!$y~0G+rt9k8wRq3FQ!?yhN#MUepDS_NDeaI-1>=>z1Nn!z_6H( zxs_4ZEDuDO<+I(rzSnUZTqG_Uv8@Bt+E*VW%+;*<$=R7*Z9WVr_q%{Tj$3b1onRPz zCm#N?gSWG?cvdA#HSz4nGC?#Bcx2|BPi@~vcfyri!x*&vJt=#a6u76w%;?p%gNjrv zGGYTpoD9wEa!e67?hSr?O0x7dW>x85W7^ACbyDvS7U@Zh<^^^Sj2Mw|w|!0a-C^|d z!)G*lAA+u2UQ2Sy7N{U0a-g1n=WXePg;rG;uk2KWpRweGhn9Z!*>Uu_Y1O^%k7J~D zcM>m6wZw-n(!_jwvVD9xWv*n3ky(!MuKvi1V!{mb`|hRX^&@uGC#*cc`&Am2hk{*2 zaj)!!!#0l|7KVxnVqsQ4xDUg)M`RR%%y*zvn?M#^Zd;SgsSeAKy9`%6P^s@^vdXIWoS(7`YRE z0)xr`JdY#JgjIcHVu?|knp?nc%*pSsx)RCS^*mNik{RpCz=2x(_>0VqA+Ocx_ z%Gc~}q6OQD<+k7S7Um@q?0OnRtePuuFDa#dc2*6-iA+`e^2fLh60amnL4y2EPZk}f z)F*o12}^Ssjj%0@M;*u>b_J?;V9Hs|D;}PKpIAdny<#yh)P7 zEDI&eH|slYylJb8MC!uW!=Rs~j}o(Lq7)4-klb!~QMyV!t*&=N2%hI(ebwv~+^0&m ztA7c{uBW&ESOs;Lr^L&jrVV6Uk79x#%LeBENgn>$+(1n0z5c6 zsPei|k;su#$!1*aNHM-*xL==D;}DpRooh;ui>H>GFB#~!+H2c2RzdoA`0O44aapCf zdp7e!>|<#)QVH_W>q6D|c;N~RV!BM=h_r5!Z03@z zPR3VF$j;)Md(y?ElKpoT7gUH(-^x1N;IogI3Z*j5R$eY~-s33={vmY%9UkFVg++-BP zuzZZ4F~2x20-v;Empk8BV6Q$I4Z%HT-BZK=>P~2nP(osAJ8Z46hy_+cgi(YM8=WMd{?k0>E zlQNrb#<9N&rcu`@ktD~J+oQUKcW)#Ro05fR#eXlCPFX&lFonDxHIr!?V%*K~>raz;V~+wz6GQ@8*5WWv(^Z zl}>xhrS4bb-0CVt4raF(s|}L13;R5w*Sm)ne{p7)XV&h`-E(n!GpDf`9uq(X+nIwJJki%sAx+3)`@c!y7fB7>c2@82!@d%fCk+(+pJHQh4m6n!j*2 z-bXzA3Ayg~ubhiOVs;-Ns0()dGOouNb1=kN3(IrA_=KEY{=Z3Qj%z=_D>n=Gfs+zb zs6i0aBbpR+KnSkbdkYsZ14Xng!sgmy9%D2|@H$>fjt^ISK^w5$zwFUZ!uU?gmG!}l z*ufKrLsP$uqhN+T%@6Kd8ZU!0h*KSru3yA_VPx6-u>j4wu#uhqw6y)(olRM)P3)r& zTIa^24o;L#Ol#!@kaJXSP0*`oHbo`HIjV=g@W$F9Ka}}cW{#Lse)Hp0foBjEY{xE$ zz-do;JhB)KKBL^6JsDIW!Yjk3EYANL*ukNVN~5pgl(f9;XvyGb6EW{e;@5YhyrA-6 zUdF5M>AP%Mvn(B(JpmKA=W9Dk5xl?Z-S20I)GUZYGvXchzV?M=%b88UmYn5ik2(Y% z!D(J(HbLi2Y-S#SUR|k}KZk55VQ8P+{as;@6)De61L`x-lQR4$)v{ zaizs8G>vN^s@jJ#K_#2^!k_u;j^)4vz9bei1r`6H$3^ZML%z{o=tZ0hixD}8WL5Ro zug5n)seK&f>;$o5HX$Xp&Wck(O^%j)Ni!GE7Tr~@)F0k9XR2jZ4Y}3Ei<+SCO~uOq z5S-qJEybi4$9B1<0;+`q-<=>Ul zw8-BNo?Uy7t#+cE@;$G421KY_&ZW3rd@oDhaRD+wNlfO1uUd6}$riPObGT5-bK6Zf z?Y6HcZXwrRK`#?=0TBvY0AIbg@#7t8J+R1U*)5-5-ceVwXfY$wnKi2Rt z?&TnJyIv`U70H&~%jN)<$tOMqo{6~J?=OCikm8O|Ghw9$?t{fefISnhrz`k|J8TV< zA4_e%tKOURS^qrq)Ncy0m)GKxF448}LZ-$fBq%%hbaelSy$Ane`q%Ezr#N$gw@=Y_ z{*?uA1>et&U8vCfe2pV}23h-tKWZ}GcC|y$()G9Ixp|p{(T|E=*RXFk; z44A?9c-vuV@9fNk%_BeOQo|i7H-v=wj6a25?a5LVm5>9pN`|NPK4KPdi%W1=7}FKN z6v%~#p6Vo+1Jat9Y|YG=Re>!9k3l=f_bfuHt^j*(EGE=|;5)&Uww>g(s-yZl8N_u+ zuFcYs?Ddx2!oRCJe7y6*e>R;>M^^JSP-I0)*ge5D?04Hnz z@~WcYXChYzr=HT3jlBlMu{$_)P?2663FS!dp5lRGuJMvLD)Gjan`NjNMlAkP$iPGH zA5F098*h0)EP$7vsmc^9bjUfVPTJm8tm+_Cve;UQxBml3RIFUjTSPh;`p#p1Kbf$N^N@M!+6)cfu-Vr+t;ZRKNyy=%??mRDdsy0OKa8ry z+zb{n9whbY!|lobm@CgQHw!x7y{D_&-^%?(hufbuA<2DYAx~ZRmu^$7axqhdjWv63 z0ni3I#TLsHsg|Q!vH?}sO;aTWE1hJAu^49uwjO*)a2H#IC)!{ZSSE+y8%Ph#I0Qak zf1(9++0s$gz|8NkQ!Pp@CRh3{*9gHtYY*ZBUW*Zafs>&P1X~Gbbr$bU5Bb^>{e0rr?kleh3gx7pEI>)QlE7~#GMkhB>%I4MgkFlyQT%>bs zXcJdnKPVH_cxd!7RLDzzZ8q|d289Bi!@J*CjKLS$W8e#~hedA8?_ZJ_Qi>{p%#{)7 zjww(y+c&=dRYaXx^EveNqzDWX|1HJYWY6~ZZBvDFGW_$)llk*Lrt#)4vm3WE#!DoJ zc6&+1HwXnSO84#?J;4@Zv#wz70w-(;TW`j6x#SE486w2aZL{k?crr8@qRTSBu2lHt z>*p$ESWW7d|9M4;F9FYMdY^fvdWBLCncX0e-S+O^k92^?e+}kve5(7h@OqZB{zfLp z&|B?V4rh-2s^r&<5k>PhenQVeE=&|ALu|suyZOeLmjhSiC-5{N;DUT!v_6nO5kK*r zutyAh37{%g4ZU({s(YUBt3;UOsP5lu6PJyM z@ga(C>RN8s8%b$ZSGRg_OF#Qoo*yboD-o_{DArYYXj+hYl5Qie@WuBI&G>zP_^_nOGqJ|K zb5N&SLdhhxFbwFT(uK-&2S!Je{lIJ1)Ajwm_~+u)v03J0*JprV{GU6MOYHyENKW|X zP)j$lzm+aTHrm&1$H@{H6}zO}F%bqoy{L=BYT}&jq)47JU40WI>mJ_&{%N|-e{MJ@ zODA5AYQ3GeMrX6)>(rXkx$n(f|7=G?)hT^>B~(0ON)p3dKpew0;^7XQ6Nqw+WT>3^ zZfU8fwzb)~Zt|8TmgE7FbO2S^y-msW;}AT)^@*56kNTlFN$S5JM0aTXW#lW@^=(=4 z;A;ob^;Abbtm+%Y@^Z(`oG;M{7Ydo?=op?Tl zr0Z(VGFq8oTm&#u_$e?KL@sgN+?kyyKfhvEjVUMwr0;$-drlVZ=dQcX<(Pw&#UEkT zNmq}yR~R@iL`sp{pV;m$kH}ZsccI?$*)m0W?QpfAsCL*J2s{-eL9v_LztYcJ#)B&?a; zlbj7(-r_0C17(SYiKpyrN!983GzNQpFx5@h{U{f)qYt}jYumDns#I>3hg%h-$|=pm#bEZWI;61 zV3P}(s+u3ke2Cp6q@4>i`V)o+1coW2sHSo7xU37UlKj%)h~RdHhVjw_99apX&dBxu z5Aw$e*E%Xx=g1%hr6MnzYH5qKYtPV~BIu8o$V?lvGi)0ez39)a8Nv_<8Ag0_7Ze0G`%$^d zq$~@74}5$SL&+yN)lJ%s{=BoLp~gNU!DWwIW%LB@Z<3aK9LoM{rsQuXxUydFVhTIF0|p znWK99G0M>e?pLbM6tPda1hvC5T?-SYQl7NGRAb{WsgyGC@X7DdP3<$6t;|k3EM`0^ zN+i1F&x-RSjk|XZb~T|6!g85>>kkD8wIUK1%`}F6l^rqhU^kv(XDKsDe4i)gq@$0j zZ@QNxpj-HE@Bkb>LA8T^em9`R;Do&ar-h~yO5*tI>?U6>zV(9<$2YgT9a}5MdaW-? zp0|6bQlguQPu)@Y>q~08gGp0q&-Jf_a~poky{#K?=~ipbQh6X$RQG*f7I1=xp=a?5 z{yc%&MMm7UI#KwY z3NcoFAq|frTl=+^onoEXJKIBXMX?_1qY{m#w}}3Y5EzHn} z`81IV6?TyjTX++6TMVqyfiG3aNcmPNaM-?IFU_>>mfy4f$VsC-L}_lfRyyMI z-`M}~@|^UjLO}d+d5r;>k_md9FG(rj<$}U4TC0KH-bj~T zGj6hPA1eXhW{%GaO-O1aVBfJp+^u8?bnjj4w%1a#FzcCAomH&N5Q}+&gP>UjbhGA_s7E9C|Fg*qQ zH(RewOLw##3ZQtl6%zSAwvva1oh4{tEyX`vFKMv5d8u}v8&u^W&#L3pF<`!^z79&+ zdspqcUNpoaB~}e8_zGv@@PAGthKUEX+t-r_LhXO|u7`Sya9B$r>L$y4tL5`M7z-uU|XQFZFB%p%baeA|a8y?SOnf zc~Vsi5nDVC3nEoiqW29@uL?R7i==}Du}CtR?E~l8@z?mC%c!g%&*ML{$nR%OP4>p8 zra$?rKehi2m0^+EWA<}tPZXoa`S%ZppKGsffW)gq;T4PL$(fro)t_L9GrR?0n58UZ z3|>Hs*}Yj3f<&0gg#*Y%gVEa0szw9Hp<<+d0y@X?F9`4fr?b{ZHMEk&X|(P1&{XQV z5qwM&-3J+3s)Mllx{*CX2_i4Dh*m1ITu&Z`Yc$%&tP8CeS0ssyuT@EpDm=|R_W;3f}GE`uHBALkX5 zk)A*Ma*&7~SHMtx@C-4xFV?-tCT2@6kGwDc>Hi4 zJ{g;?*LY={yb}o2+C>%*DXbkFfXx!gVbU$ve(&*|*!Wia4aX;gSc_2UO52z+I(?cy z^3GHy-yAZ_Yx5y>^(kZIB=x`z76?Nfy*27aA367x> zq*3sq^vjka5)_%BoA1s8U8K*rOom^4s_{AAk3uyK##b#c!f_X^5Ym*-T0dXyW!UQ* zSHB^Y*9I_=d+wx0@&|d?^K+}knPsb%81y4Tbh$8ecR7QQq0o+j6N2?n3Vchp)_Bdz?+1`I7(+~M=c$90N(LR{k-6$_|ti*+6pbT?_leWYTgIlEnLfZL!a9lq zvsGbriH;(UX@K3GjKoi2io%=d3AzzmdD+^!^2g*wJw3B1ib!r1&VwZ8b0@5WyO$b)EJ29jyv7a9M9 zp_CLmPLJ_Bix}K8VgJ-C(<_2g0q)JgLUT)-_Pq z-iBX+DH7`Lo=+%#)3tQ?H2#Q4Bw-VEVCg3Z9D0xj?14m!XDHKP&0Ee)pH-w-%~xVA zDPYjoFm2<*W5JsFSWMU69M(YLI;v__=49s2)$UCmbnr-8an$oAE7w@0njsej_DGGL zfmt-b^r)KN{D+~&DOLN9(~N;qAYf)^A<>FJrngPHd|rLVaessG4VV60+csU& z9}9a7CqsNt+&>(J|fV42u4!0Y45hhde1NC^C|m&zX! z2PWU3*RSv4g5z`ehLtlynKYzJ)Zfj{Ljm^S4pQTQwqIN7Sk_nBps;n#!8(16`;)Dh z?8G&BjA>5l4L_dF+_;fU7QWoDM1Y;P|fWb3U1bOthz`%vL?Uhm&qB+cA2kT)e;&+nh){1RugfP+gS+l)!2#nv~2*7V28h3PEFBY{!ol4IU zAS$Dch|MPOGl{J`XI5h#YNqYR9c#__R_UHMr-Vh~JR`rIcv|lV3HSAFd`X+$X>yK> zUzF9d11||tUT47ftzy5!Y306pV@vgTM(AT4UPDX4b3OwOgoYh$6OXX5#|3x1x?lKu zywxpqyJ5u$F`HY0?t$mm^x3{1cxD7cU@SBYh3(3sePuG%fm1= zj*g${oOj;@xn`^OnfE$ktgfPd+;XKHUwf+E%Fj(YZ01XnG1%RM{w;Xi1Zl4I$|Y0l znp0{XVX8wRMD)8V%)?Ew>2F{cI6)V&g;`m*Ht8B|@izg5DmM73=x3>3zA2sb0!zM^ za4^kd02zj!m1@1Ow8Q%SJ-%h+Qv1VA+hCrZo?g5^TpJ@kPo!%PSxtL(nN~O_Yh9sx zWa*tW8UOdKSoq57@hP$%()XO)wOnPLbVvX(4cZws(p9tpcn)vs_V1*H76BS;VZ;v! zlHoIZieM6>O~`WI*pR8GlYScdZ%)lodV#AO1c}ZJfoWHp;*pbe%cQQ%?9yK|GBux$ z8{N9MFaMiZGka~g0sC7C7&T*_yr7ijHLu&xS|{H0H#XYe-I$hqnSzX7YHNPegeJ4B z5;c2sXtL!c8kM}@O(M{&t|oXSxy3UP0M%b+cVeBRs>8mSw9Q3Do17LSmO^#ivQ)cV za$ZgO7ke-HehWRe;I4mjP3rerQm$^Gx|UX+n0g*rKB#=-Pouw6JD4f2@o$oJvxGBF9yXxeRG~qC9&vlXtr50(^6W-BCgkz- zfv@9f0XemeORw~_FnC=CYgVX^pO9hQKn=~?2!&sH=RPdRa=*yn(w`1{tazbS*V02W zvwyQ5b~~2psdk^OSk2IvcenE%2P5^VOjWL^_ktd@&^?|AY>wL48%G-&5^kxeyvino zDI88o^@7j1f-8bETU_0e&b$vbHXEg>6`0z}v5kHZBguhzHF0!OD2fmLq? z70WWLfL584FO)qTfwC42BuIX$NxQ}^;##tvcw-tM4MNxL4_|N2@`h&p)v~0(Lh0zO zl4!M4LCu3MQ3*40r}dHvw}q{sYjQ9<)7fizIPcidc9@MM-|*v_`wh$e6}Xw6Y}&Wq zg33EqHa<1M_SYZM`p-a0;i%BzyFqKS z6RLUPQ{#|PG0k;&?H(yt0e}5@e)U#9w)^PnP55hNieb5Qe*WUq$$@Mmo^M6gUox@_najPc&_?`E_Py0Lf zZNCIGxR4Wp2SoemEORuA85ZnV{hoEBs6B^PDqP_Z_H3T6H-tXe-i|WgwgRww$f`(X zKmv)?*gcJl0+_5mgL-AF9-g!Bh@JCG8F79Um@rfvxnQUnT9BGtu^E0@3I1ZsB{?Dg z)t3%hpret;8|xNa&mKG9@LOD9DfbRXQ~_DqrD!+GF z-St%y#%a+_B1g-MS@!hRnjuzi8;s`aOCXNJ`peMfD4Lzoqc$&6cGnXEwvJPv=P>8c zbLI0a`xi~x9m(K=FU)L?6A_M9>dw3JbB%H1d!Rh-XQE^&1dSy;5L_t@G4ek3PEX6( z1bQo~t+M}?`RCPMM?E@HKw)|EExqJEUp(*EEi}RT*F(?uSB2ru^~#fqH(xHI39~D7 zQ(@VJRdAndCVuD8mlK6MT}6aY*SUp_=4MI{F&wshrc!860qhc-IMO8S0heEY0{2Bb zT2nCbB>|#!elcqF9b$cSuZ>N}?uTXP=3zJ+lL__MyQ!jeo3~U;xvbdWx!fvUkOIVg z9P>C$jZqf?oiTz9G$z;khAv&$ZM#)aJX$72n@-?sZGgo?G3Ph$vyj#w{EC2?Ypgbt z!SgFtz9cBeMku0!RREFTFmosJi!wITZ(8yvQ6a2A&vES-S-ypVb`rqI#slZzd5vc( zj1VU5Lx>R5pmC5sn=j7i?`Qg~@(hQJzn9_<>`^fkt~56pk~#rGMjU-xUJgw~juU3$??n*Eby6^ORGtqYvt^v;77sbb&Bp9VPq5MursmWei6PYiery*=uMlI*CPO!| zROEw?=h7}2GlZhG3M0~FE|)u6t1^<$FiQL8|16xLGWz76qID%B<|Njq^{;a6&mMK* z4QaH~f4@{PYnJZ~sM-pvG&x@<8D8#q3oN8^`;gs>5w9quAhs&~Tl}jp@1%8`F!@$} zCOs&*s328@jpK7iEk`qN@(r||p1 zp)vt|4*2?CGdADa1>7sXD*(0jF1*$z8*HZch_PYT`A;0|!4dzMm=a&om4%>R<(GEV z1zw0PNz`whQ*QaRtCP??4jRAbI7C?5ZlzE)c)2a>-PV$f+a2UKc7ER&uF_0=2Mv1AHce%O^%6~s zHzhb{R&yhRihW=H_HRRXq`Q&4h~^Nzr(yyNE>CNoZgA}`{q`OT{-TVe2xb>1<-ckq z?!ZdM9Q*Usa#?~{M$p=_)&HfkKmMVzH%W!$8n6D}6VAJ4riqV+RbZfsW!`l8d#e!u z>F1TNIxd>v4L#0>%b28CFkF;mqaF-{wRw$R6efsobrqfZuRP|UvQZ)F7Cy5MOkJfx z6p0ZBOCi5@(;pq1YQQ@fs`?l!j=f-`_pra^{BZD&Lu%BXS4fSw7ySXrG%OBz%C&p+ zd&;t+n?Bo5rzV-UW_&|R-rAnBTZZt_`JQK}vEf+Ui3vWfSqpJ{unR_xXNBGh-YkIQ zjg|JCx2oFics=$k@PH}oFpgsjn$Ss)!3U&2f_9 zwqh#AALZ`Bsy^84Zir5}GwBK&hy>mR4Oa{rZ5v?V$@EuWt#7GJ>; zSz0XW<5ikh2naVrZ3;rev=OnumP-~J7VqD_`8puf*4wjfVFUM{e3_db@XnK78GS+5_c zJ$bMy|G&xRrhBX2Ik72}_TF0}7byU*U%r7PQ ze(4;|Po3mZgYFvGSCs%HBP?BC0dZ1~?PsoGLD7H~6yudP`R^P2<5>K^iY3n)oFW~h z90v9);!^vaC>z`8T`3!BQ!EpYw-?)I3u}-dGbw>d*o#e;^Y`K0e?p`$0nZw40bL}X ztqFPLDfd$aL)+W>{7a8jL~CjiPcAHmbUh!&{)xQEXz4~3PPcHreyR>?($7I~5?;+~RB3>&Fo|^0`X33mQX+g)$1Td~+ zBlpJj`)AUIe~jNlm4nFS>vHpVh>jxlOy15>eWh*8s$_h3lzC-;2U_vEK2Z!>S-C!n zfQiD{>=CHLEe>E06h5L5<2_HWa3)!Lv9;>*6+~^rDmA2)-N^hP*UpaS{wcK z%>vkbAFL}sVd$216WW(3v!`1TFd_R8*v)K~8)`E#GmY)+OL#p`WmHkM?%g7MpYU%w zbEB$<%BbXplz-)?(PsN)2|hy$uM=-sM@r(#QDFML`oz}bZE^SM-5gcVFf$M6<=OwS zebtiTZN5fpsd|NPk9+g72hwKWJR(!pO?-?sSfra%EP&82x}5Xwlv9iJ!T(lw8yRuS zR_#q1UXDxNp5ilEtno&9UjJQh zqT%^(TTrl^o~iBXZmejO5bVal4OlJy+8$Vi589og)WxE=iT>na zb@|5sSU`&F!Xsp~BeK3Lt+|`ZXTv#)DH!i45!eT}G2_!;tMKe(lg^C=#zSQ?_7Q_0 z=5S_<{(Qr2{kt^hFDt43nLE7LA{Pu3=+DlOTpi<%0+8#8=VJpC=dS#zPLVD7Xrlo8a?*@DS&}odZQ(n}@ZBwuZt;?xfQuua}wq zV&3bsN5S$RHj$!U^GF-pu&IrVw|Sh{`x^(L9X=<)B-;u6PJcQPk3}!PwOFBDA%@lv zNSI}@)~yHTE2e{}=0Ft#xNM9($xpDR7da<7|8?)~57fPu{au`h4N~W>d+Q)>v?Jl! zESo!Q!+gfQw6r>XVebz5lzf8J%(km-W#;6@JRK%x?qJ9Jj}mr)V244;y^QrCz9g-% z%q4#Pf`XUw3=O@xZ&Q}4HC{g4HA-;-dZ?(lFIbF`S%`^^uz6G-J+iGwjRa=yqPALUn656pWWw+%*~;^|*+K zJK&0ZFGr8Y#s2O;its@>2cnE`EG12J4iSk49|D#k#0IF|m>TE0rld!mBcb*1wpu%{ zGQ)rX_9ly}G1RPbP6F^wE`v|)RtR@4}CAP~>P@xXPTsr#B=m@n3+8ovxtnajty_trFiZ+T@47CPL^1EHpu zT-ufI7oBAJE`wjFXzGo*&*Z3{`|2*ShMyP`J5?}(!2UQ-mg*e4)t5G`4gSnmbW(iH z@%}qtue^mmB+H@i_8Z6C&0= zhMe%O;hk=@?Rkm1#2}k;a&)fDrsAREewOM4Roh0JdJQ)5k3Qwj)omLdFi><`xCaQoqb)r5 zx@lu_N!fsYLs_RmMeAtkVR(GQH?<$QX+VmBMZnjimhukSowg}r9n8ve7Bp~5gMJ|{ z`1yD&m@zlLaOU1@5ZM}vltWH3IJooJ=MtZO#%{l2!1eP#v4BiOeR*Q>(5+e3ivZ!H6gk6mWgjttZ;8dgO;DgKz5{6ed~ zhdW==Ecp;Dy^yUDVvC5$iFNFeqaww#t!0PYe8D|5#x+$h;SLJH$T#7#2To?XL$V)r}IR|*h-;gqI{Sm zhnF{Pk%aSZ9`~!&R8f$WCgFI~kGTN$NcI8YCdRqOAcO{X>tC(E-)6d&zs}EMNll&m zt7>c4*-xt&ETyG3D@d&7DemCBCpoEm>u!p5tS5?KWrx*4`!Ts2-9SG={3KZnJ#dGG zoIrd$x`nG594Q5i2dmBhFWTNb9_lar8!jnSqO{mWq7sGd%ZQM&Bq2MEEezS$F-elb zDA|{>lqJh#8#{?i)*;JaF!qr##yVq+;hw&~`}d^p?|Gj4xqr{={=+|BAI>?~xz0J) zd0+4AeNMkpcVk$_i~-rG3?Pr^gaTJABRAz8hzX11wYImxx zpv(~9qI@U*AzxrGgcs$*)6~!;;edfL9+>998R|wu8u*rx)CH0HLpKk>E;MIeZTi<) zMs$Ab-)7L$z&)if`HAT{(>m80zGLP9&ueaUhAQ1y1O{{=jZ}fYhYp1(3-B~&-Z=P= z{yREW08xtn+ilnz_g(wVM&7;nfkv#=6Ua=xuxiso$WGzfISN=@#lijdF7Ixy;_xrg zJ~XL_umJe(PWFwjdP*LJX(kn;U#7s%$XEwkZ&evv<32|h;&Pi`a-|*^;%lP8Yf3`4 zg2H(gqKyYO{3Dg+*Uq`zH#%k9Y@Lfb3t^aQZXS%E!^h1JwF&gXWofw?idj47(9nH{ zQ1PlJQ<6CCI?@h~{i55{!}Sh9BzXiJJFxY%k)3AI^WQnSWutlv3Bj^s&FPN}mT_h0 zsfo4s#59UoJ}x-Q_7)5FU-6bcSJ4*hE4jJLd4AaQ*v3x-aOepaFq~jPt;=w7Eq8Dz zUUNiRr+W-7gjf!;ZJ+$B717p`=Y3k=U+HGu_x>nvl4yWIzck+zOgYQqx-dd3-D5HI z(3$vn1!u=MyXk*RB{S+ZkxL%Zg&j*`I zEiPp5H!987y1Z~!=4eDn3)(2Tx%J6Tq|1}gRb=?ya#2cB3 z#ju04=tCI2z-7ikaVdKe?KSRTm8^#QOK=1tRsgGIZ-(_ASXlIux;u=HS`&<&980ND??t;D)|Z$3MIKx(gL5-X-gyQ(H=aDm zw2@bK>}5{AE|biq%jV~ddA(;tURoO3ln|fT_Zm2owz-`VLdj`9 z6Bw_+l8+C@FJue=g}<3y{HI3fEs4ml$S(mZuWC4PuRGlPb_hgb@E#YB>zN(ulVKdZ z_*CPPkaQVnVu3PT&dVc&l?f43@?Mi9J5Nwk^lM zGXI8BG9i~nT@QUrSx(+qO~rItN}k`oc{5BO#ob_I{UUPe>3-Mz<(jMCZ5hs`)6*s* zeca=LP9eN>Tk&s(XWuLSTsc$G0I52|`~0_|>|c*m2)DhtqBWWjU1u4jfmT6B(J2SK zDFH6zdd2Xd`#!twLbGf4)^xcCC+MbfqxJ9F2XM?WOU)WQ=(ZVHWFgT;;igXOZlFS- z9#@JD=|dQbk!~enru`)So%{>?!OyQ_Q~HkGe%yYnO<^k0_iUG7b-BpJH#~0|PtV^5 zvFJLi>im zix%F)?;OMRfZ?nm?<)0zc(s^|; zQi3l-`jHtbht&6?{wE#0X@Me)oLs-X8k~q$`6nyzB4B9BtT55@Sfo_ga%;$N^)KQ# zMk%UEI#*ehtKO-h{G$msNBpjLs);@5x~fMlKV{rL`B-m4#lfDzsPYra^Q9Yy{_nbq z@Or0`nt*zwr&E)t(cN*LN4pqZKTqPJ0KeBnRLA2Y$3&d=%%!g*whS4W<8xP7p%goR zaCN22YVo5k`BqJO@ciMfZ=Sy!z5HHf4*vS1ZQvq|T_EjYd&JGOB{}JenY(r-l07vF zqa7SE%A;``AL-nSlpiCVs3`jQr!BwzqC5Lj_O(#0ae-19k10U{!@}k-(dA!5oJ0EQd7dMtm05S-S5DVa!!^W zMgy`g_|CO=*1?H~o3M=KB1X3(SqArBo7^;zqgOrcCM3SRUOG(1u@fxM&&gMH?7|%BKHByzlBr81}{K^%u^H}<8{O=W2Y&dfS z^OCKy-t5)}zw-T!mNe!xplXt#NMF~yc*^Z-4~}1QkcLK^m6K7N9K!?@wd$ejpns~G z*=S?w^@6uebVRJ@S4c>p_mX5lQ0E*0Pa)ypuj(A9(&;PP=VPSn>DHue{x((etLLS_ z+uxlm=+!v%EJn>4Cy9%!fKUE}7olE0{ciN3+dk9XIQNaI5fFoU8^LGT0 zdE|defKBsqYFxhQ@S*U`upJ`bI3pA?ZXI(I+Of1ws}j={12Y6j{r;10Q!IxBj{i@M z(`U!iSuQnIN zZ|`&aV#id!GX14U)9O7NlgUeUwU*(yOR!KqX2PwD=uJsv;n-b&>}D%oqdK(2(tdRH z`rnNV77GlO=lytPRy(VkaL!@*V}e4{%H|2Hl)wS0usvpttLDb`(5GBRB?^ZE&K!)n z`~w`*1!o?!rVZU4ZJru^vUIn3>Rj>7v8Ko?T(?GVgeojwP;sJ~x5r3@xfe1`as5?u z^)IBZ&HsO2vF(5TFM7LnHddFMCI<`}OwAy&O{5JXw#jC4@Az2N7H20c(9}U!WhQ^- z@ZVwBDcq+g3}%kG+FXiT;J5FLxaBtx-CtO`<}eGg>r%gi_2(N0(5v4IvG$vMA*ad^ z7!qn_O&9F?4LDMQt?V|}yOKr`9a2S-A9g;Er9&x(}hg z(XSHK=k8GUK^AG#jL+!)`lph@U;YN)F})ne%|X&=f*J#L?aODn5ZDcqqaaI3{^z29 zvCbnmyds`Hln%N^9rvf&U(f5U#D;8km7YA~Ysb*?`d{sr-qgFMWk5r>R33ix#Pq}` z8(O)|NxGaZ@Bl3MA49zLG*xZBsj?GUx)AjPuvFzH4cOnC)K)!pEcd^Dds@rD^-sXK zH*q^e@>VqSpKBytf*3bnCh}k>Icfi=lMQ0q%bAj)wr~mfnLkvBU!P9hJs?($o9`ES zFechWL~?1={+jLDzLAV@DCIoyHU4k^50u2#1Fx9O+=|Jx#lq~TnB$#X@AJ8K)<8qI z5)aF>9Sn*84@x5ct!?<1y?@u{{Rr8>VISp z8+T=qd^uP+6PiX{-yt9!`1Ca)Hlcf8fa8I_e5p%4o!bm4iFM%9*t{FwxeI}tvwfiJ zz%`5q8Vi6}Hqo{NpXyVYoSR++Q`gC;F6eNGl}vT~PVa`YlLZ&JhxB2zGFG!HV(0H% zL}-2|0w`GWr?Kd*f3o?2pfj&VQ~YD51LWPvrmvtV%1Mx~y(ZGU zl}{6UNWK|P4nh)W;DmFfL%GW6o9wCgT6_n&&YB_38sB~ti1e+`iYx#hQp(Ch#+pPr zbJ*s!Z9{&#(xGgl6ldNFMcu5e?Q!M@HG*GzkR*xuBddG_Mn9RFWU;+3P8uS zrzB1VrE<)`AVnvz))g6%evrF(8td^W?&dJZ;y8Pm4z2V_#5?+ZeK7EhE7?r*Ka>xP zY5RFU@NbsZMA}7^96wb>(^W~%_Vj!nVKhgf2a~26ip~N3tko|G$4I{$$~6(~pfo(( za^3#vAL>PSXRRe=GABKl?G9`Rvi$LPDwC#llm(~~*2}T=`wHLq$!v~(g^x1`kt-3q zc1uuGGwg%pGdQkuC8s@OHykO= zI-XLR)|yfoX=*YX_75vdAX_$2z_Kfr8V!lMhH3t%$9DeIx=M5f&l(=(cr7ppuj40H{Vb6r*~=bx*JVyp{Wi%O}54LgRZD1_n=NrO*G%q`M(mJ zRKi0sV*UD7JsJN$p?0;JucOCT=hivLD(6AHNx zPyQ(AimvcWzuOLi-QP^N`ZN?^xLrDyphn_N^iI`HfKjTldv#>7eho(8$*Hva61eQ3 z(o&-k*0-*j-Dznz3`MTFBCQ_Q_eOLKnmNYTtn~T*Ya7xYKFO*1w(RC)jjDE|n5%pI ztaEyVX)yaS?3*&=a|Szh%y{p+Jpz5i50jDhxK6>YL}EFS*N z{Am2U#s6MO%OKZm@E)J?8p;f?*yY8xFElwxoHBzPACK`C+`9}Py8*5WMNI5(QrZNu zkVll!)fA0p4W~gH&yCd3GqgU>yp6Nz&?Pe8JfOA-9BIf(t9#SzUF1uXNL^^-!*Zg_ z0YO%!@b3<`LuiL5_|(RgT#~`HgL_o|TjD}eQI4?Bz1d?*wd;j3=az*CgJ}C*Esd{$kIuZoKPbkIkJlW4r;5Vq=Z$WGqJ)(UhJBO>Fy_ z&weJu*Ue8-B?;y5fvQaBJ^d>O8^paKhH7eyvy>2mC5BA!!b4|JzQy00yb@ljxapMk zbe^d1?AQZ;{H!Berz0~ke;%~;9c{fOV}du>*i901D=z#=9~}?YhRM+aS+hhd;x(mf z+PZE&gynMl#eXiO^PksC4b*$I@2&$KerHsfH3fdUL}aeG0imUT$aWQU!te|m1{aLn%1qLcVs0oIQnrS;P4ax@Zj zaBKday-A+u(P8pv2aKQlPtF%mwG>qfrN+9pUq~9%H?7Zvt(k-FD%D z$olsXx&Ts)rZA?cth_2MI&nFu0tb-}B8B@6w7Z{xw+3pAl1vO_oegHm&hNrC8stB2cr3<<_3DwMPz{@u;6J#Bw*l>tIZwXb^(#FqUDF>BzZt z;^S|TMiwVKhJ=L|Np!Ve;KQO)`8D14@R9lVoxVWrGj#s(lTRN2E5}9?#9lSBs;ta3 zyp=rPo{?gqurJGnm31N%Nb%{^O9%W!+}pR}3KaL{=C1xRRE2P`cI_Hj`mt9~`{50e z0N1eayJiZo>8~+I% z!jQH*bf!zEgcdf};49L9Z)k4&xGy@F>Vmp&_pzoWHI=(pFgnin zDqW&13RT`+V9AgUo4He3+x+0eh{zVX`2lEhS?lR7PGjU;{PLGl7nR!0hMJAcm;nM_ zCakzcx+8BUX+L@Md=7P($@j0|k z-8h}lQu7%z!#<{`%0AJ0T-wA8?7%b*ELLKfm@ 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 Date: Sat, 9 Sep 2023 13:15:22 +0200 Subject: [PATCH 28/30] [Docs] Rules example for PostToHTTP/PutToHTTP --- docs/source/Rules/Rules.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/source/Rules/Rules.rst b/docs/source/Rules/Rules.rst index 7509f01408..a405c1fe9a 100644 --- a/docs/source/Rules/Rules.rst +++ b/docs/source/Rules/Rules.rst @@ -1957,6 +1957,41 @@ Added: 2022/07/23 * HTTP user credentials now can handle Basic Auth and Digest Auth. +Convert curl POST command to PostToHTTP +--------------------------------------- + +Source: The Letscontrolit Forum. + +Like the ``SendToHTTP`` command, there are similar ``PostToHTTP`` and ``PutToHTTP`` commands, using the corresponding ``POST`` and ``PUT`` HTTP verbs to transmit data to a remote host. + +When translating a known ``curl`` command-line to ``PostToHTTP`` we have this example: + +Curl command sending data to Home assistant: + +.. code-block:: none + + curl -X POST -H "Authorization: Bearer VERY_LONG_HOME_ASSISTANT_TOKEN_TO_VALORIZE" -H "Content-Type: application/json" -d '{"state": "off"}' http://192.168.1.25:8123/api/states/light.shellyplus1pm_123456abc123_switch_0 + +Corresponding PostToHTTP command from rules using the 'Format 1' syntax: (formatting Switch State value to on/off in all lowercase) + +.. code-block:: none + + PostToHTTP,192.168.1.25,8123,/api/states/light.shellyplus1pm_123456abc123_switch_0,'Authorization: Bearer VERY_LONG_HOME_ASSISTANT_TOKEN_TO_VALORIZE%LF%Content-Type: application/json',`{"state": "[Switch#State#O#l]"}` + +Corresponding PostToHTTP command from rules using the 'Format 2' syntax: + +.. code-block:: none + + PostToHTTP,http://192.168.1.25:8123/api/states/light.shellyplus1pm_123456abc123_switch_0,'Authorization: Bearer VERY_LONG_HOME_ASSISTANT_TOKEN_TO_VALORIZE%LF%Content-Type: application/json',`{"state": "[Switch#State#O#l]"}` + + +Remarks: + +- Multiple headers have to be combined into 1 (quoted) string argument, using ``%LF%`` as a separator. +- Authorization can, instead of including a ``Authorization`` header, be included in the 'Format 2' syntax like ``http://username:password@url``, this will be transformed to the proper header value. +- Similarly, a ``PUT`` request can be converted to ``PutToHTTP``. + + Dew Point for temp/humidity sensors (BME280 for example) -------------------------------------------------------- From b7b4925649e3ba4b3e96418a35d8f143a499a1ba Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 9 Sep 2023 13:17:44 +0200 Subject: [PATCH 29/30] [UI] Bugfix: Don't show - None - [SD-CARD] in GPIO selector when SD-Card is not configured (improves #4780) --- src/src/Helpers/StringGenerator_GPIO.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Helpers/StringGenerator_GPIO.cpp b/src/src/Helpers/StringGenerator_GPIO.cpp index 7823293203..aa8835ba2e 100644 --- a/src/src/Helpers/StringGenerator_GPIO.cpp +++ b/src/src/Helpers/StringGenerator_GPIO.cpp @@ -274,7 +274,7 @@ const __FlashStringHelper* getConflictingUse(int gpio, PinSelectPurpose purpose) } #if FEATURE_SD - if (Settings.Pin_sd_cs == gpio && includeSDCard) { return F("SD-Card CS"); } + if (validGpio(gpio) && Settings.Pin_sd_cs == gpio && includeSDCard) { return F("SD-Card CS"); } #endif // if FEATURE_SD From 8136143953b79b17d9d035055ad2fbed2f768966 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 12 Sep 2023 22:54:22 +0200 Subject: [PATCH 30/30] [Build] Restore RTTTL feature in ESP8266 Climate build --- platformio_esp82xx_envs.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio_esp82xx_envs.ini b/platformio_esp82xx_envs.ini index ae980c9fdf..4e070b7ab6 100644 --- a/platformio_esp82xx_envs.ini +++ b/platformio_esp82xx_envs.ini @@ -817,6 +817,7 @@ platform_packages = ${regular_platform.platform_packages} build_flags = ${regular_platform.build_flags} ${esp8266_4M1M.build_flags} -D PLUGIN_CLIMATE_COLLECTION + -D KEEP_RTTTL -D WEBSERVER_USE_CDN_JS_CSS ; neopixel : 4096k version ----------------------------