From 806e307af819618113d5fe40c56157b38fb2e2e9 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 17 Dec 2023 12:33:10 +0100 Subject: [PATCH 01/30] add class id --- src/IotWebConfParameter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IotWebConfParameter.cpp b/src/IotWebConfParameter.cpp index 83aa153..b41cff3 100644 --- a/src/IotWebConfParameter.cpp +++ b/src/IotWebConfParameter.cpp @@ -337,7 +337,7 @@ String TextParameter::renderHtml( "{c}", current->customHtml == nullptr ? "" : current->customHtml); pitem.replace( "{s}", - current->errorMessage == nullptr ? "" : "de"); // Div style class. + current->errorMessage == nullptr ? current->getId() : "de"); // Div style class. pitem.replace( "{e}", current->errorMessage == nullptr ? "" : current->errorMessage); From 26dcd165c1336b49671c8c573b91d01a75100c0c Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 28 Dec 2023 15:55:49 +0100 Subject: [PATCH 02/30] Date and Time parameter class added --- src/IotWebConfParameter.h | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index 1f83691..5d79ac5 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -432,6 +432,62 @@ class SelectParameter : public OptionsParameter friend class IotWebConf; }; +/////////////////////////////////////////////////////////////////////////////// + +/** + * Date parameter is an option parameter, that rendered as HTML Date picker. + */ +class DateParameter : public iotwebconf::TextParameter { +public: + /** + * (See TextParameter for arguments!) + */ + DateParameter( + const char* label, const char* id, char* valueBuffer, int length, + const char* defaultValue = nullptr) + : iotwebconf::TextParameter( + label, id, valueBuffer, length, defaultValue, nullptr, nullptr) { + } + +protected: + virtual String renderHtml( + bool dataArrived, bool hasValueFromPost, String valueFromPost) override + { + return TextParameter::renderHtml("date", hasValueFromPost, valueFromPost); + }; + +private: + friend class IotWebConf; +}; + +/////////////////////////////////////////////////////////////////////////////// + +/** + * Time parameter is an option parameter, that rendered as HTML time picker. + */ +class TimeParameter : public iotwebconf::TextParameter { +public: + /** + * (See TextParameter for arguments!) + */ + TimeParameter( + const char* label, const char* id, char* valueBuffer, int length, + const char* defaultValue = nullptr) + : iotwebconf::TextParameter( + label, id, valueBuffer, length, defaultValue, nullptr, nullptr) { + } + +protected: + virtual String renderHtml( + bool dataArrived, bool hasValueFromPost, String valueFromPost) override + { + return TextParameter::renderHtml("time", hasValueFromPost, valueFromPost); + }; + +private: + friend class IotWebConf; +}; + /** * This class is here just to make some nice indents on debug output * for group tree. From 82958eca0a377414d9288abbbbab0b9afcb14f4b Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 28 Dec 2023 15:58:51 +0100 Subject: [PATCH 03/30] file updated --- .gitignore | 412 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 411 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ae095a1..f3aa2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,412 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Files built by +/sch/*.b#* +/sch/*.pro +/sch/*.s#* + + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +__vm/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +*.vcxproj +*.filters +*.user + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace .vscode -examples-pio \ No newline at end of file +examples-pio + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +*.heic + From 15d6cf01109a2c80bee98afbb4f94e021b3c6c3d Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 28 Dec 2023 16:59:28 +0100 Subject: [PATCH 04/30] fixed typo in new classes --- src/IotWebConfParameter.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index 5d79ac5..b5cf265 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -437,7 +437,7 @@ class SelectParameter : public OptionsParameter /** * Date parameter is an option parameter, that rendered as HTML Date picker. */ -class DateParameter : public iotwebconf::TextParameter { +class DateParameter : public TextParameter { public: /** * (See TextParameter for arguments!) @@ -465,7 +465,7 @@ class DateParameter : public iotwebconf::TextParameter { /** * Time parameter is an option parameter, that rendered as HTML time picker. */ -class TimeParameter : public iotwebconf::TextParameter { +class TimeParameter : public TextParameter { public: /** * (See TextParameter for arguments!) From 4175f41875689f43e5bd0dc672c09a4d86982cf4 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 10 May 2024 08:37:38 +0200 Subject: [PATCH 05/30] url updated --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 2c50c84..67dbf36 100644 --- a/library.properties +++ b/library.properties @@ -5,6 +5,6 @@ maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. paragraph=IotWebConf will start up in AP (access point) mode, and provide a config portal for entering WiFi connection and other user-settings. The configuration is persisted in EEPROM. The config portal will stay available after WiFi connection was made. A WiFiManager alternative. category=Communication -url=https://github.com/prampec/IotWebConf +url=https://github.com/minou65/IotWebConf architectures=esp8266,esp32 includes=IotWebConf.h From d6da95b69b894cc1a10b94fa36b3018d5c6c27f2 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 10 May 2024 08:38:29 +0200 Subject: [PATCH 06/30] updated --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f3aa2c2..9a3c7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -410,3 +410,5 @@ examples-pio *.sln.iml *.heic +/IotWebConf.sln +/IotWebConf.vcxitems From fcc4718aef65a80de616212772a3af85b3045fdd Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 1 Oct 2024 07:44:09 +0200 Subject: [PATCH 07/30] fixed errors in stop() --- src/IotWebConf.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 61935d7..3afa7f9 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -302,15 +302,21 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.println("Rendering parameters:"); - this->_systemParameters.debugTo(&Serial); - this->_customParameterGroups.debugTo(&Serial); + //this->_systemParameters.debugTo(&Serial); + //this->_customParameterGroups.debugTo(&Serial); #endif + Serial.println("=========================Add parameters to the form"); + + // -- Add parameters to the form this->_systemParameters.renderHtml(dataArrived, webRequestWrapper); + Serial.println("=========================_systemParameters"); this->_customParameterGroups.renderHtml(dataArrived, webRequestWrapper); + Serial.println("=========================_customParameterGroups"); content = htmlFormatProvider->getFormEnd(); + Serial.println("=========================updatePath"); if (this->_updatePath != nullptr) { String pitem = htmlFormatProvider->getUpdate(); @@ -319,6 +325,7 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) } // -- Fill config version string; + Serial.println("=========================Fill config version string"); { String pitem = htmlFormatProvider->getConfigVer(); pitem.replace("{v}", this->_configVersion); From 3939be49ce6d44134b598c2ecd261e8d7c6ba76f Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 1 Oct 2024 11:54:08 +0200 Subject: [PATCH 08/30] migratet to new version ESPAsyncWebServer. Added some error handling for more stability --- src/IotWebConf.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 3afa7f9..61935d7 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -302,21 +302,15 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.println("Rendering parameters:"); - //this->_systemParameters.debugTo(&Serial); - //this->_customParameterGroups.debugTo(&Serial); + this->_systemParameters.debugTo(&Serial); + this->_customParameterGroups.debugTo(&Serial); #endif - Serial.println("=========================Add parameters to the form"); - - // -- Add parameters to the form this->_systemParameters.renderHtml(dataArrived, webRequestWrapper); - Serial.println("=========================_systemParameters"); this->_customParameterGroups.renderHtml(dataArrived, webRequestWrapper); - Serial.println("=========================_customParameterGroups"); content = htmlFormatProvider->getFormEnd(); - Serial.println("=========================updatePath"); if (this->_updatePath != nullptr) { String pitem = htmlFormatProvider->getUpdate(); @@ -325,7 +319,6 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) } // -- Fill config version string; - Serial.println("=========================Fill config version string"); { String pitem = htmlFormatProvider->getConfigVer(); pitem.replace("{v}", this->_configVersion); From e16d16a54808d079e7a96af000502c0d679ef5a5 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 1 Oct 2024 11:57:33 +0200 Subject: [PATCH 09/30] Revert "migratet to new version ESPAsyncWebServer. Added some error handling for more stability" This reverts commit 3939be49ce6d44134b598c2ecd261e8d7c6ba76f. --- src/IotWebConf.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 61935d7..3afa7f9 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -302,15 +302,21 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.println("Rendering parameters:"); - this->_systemParameters.debugTo(&Serial); - this->_customParameterGroups.debugTo(&Serial); + //this->_systemParameters.debugTo(&Serial); + //this->_customParameterGroups.debugTo(&Serial); #endif + Serial.println("=========================Add parameters to the form"); + + // -- Add parameters to the form this->_systemParameters.renderHtml(dataArrived, webRequestWrapper); + Serial.println("=========================_systemParameters"); this->_customParameterGroups.renderHtml(dataArrived, webRequestWrapper); + Serial.println("=========================_customParameterGroups"); content = htmlFormatProvider->getFormEnd(); + Serial.println("=========================updatePath"); if (this->_updatePath != nullptr) { String pitem = htmlFormatProvider->getUpdate(); @@ -319,6 +325,7 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) } // -- Fill config version string; + Serial.println("=========================Fill config version string"); { String pitem = htmlFormatProvider->getConfigVer(); pitem.replace("{v}", this->_configVersion); From a8278da9ae2d208df9e79094e1e5ec6fa2fdf0e8 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 1 Oct 2024 12:04:56 +0200 Subject: [PATCH 10/30] removed some debug informations --- src/IotWebConf.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 3afa7f9..61935d7 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -302,21 +302,15 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) #ifdef IOTWEBCONF_DEBUG_TO_SERIAL Serial.println("Rendering parameters:"); - //this->_systemParameters.debugTo(&Serial); - //this->_customParameterGroups.debugTo(&Serial); + this->_systemParameters.debugTo(&Serial); + this->_customParameterGroups.debugTo(&Serial); #endif - Serial.println("=========================Add parameters to the form"); - - // -- Add parameters to the form this->_systemParameters.renderHtml(dataArrived, webRequestWrapper); - Serial.println("=========================_systemParameters"); this->_customParameterGroups.renderHtml(dataArrived, webRequestWrapper); - Serial.println("=========================_customParameterGroups"); content = htmlFormatProvider->getFormEnd(); - Serial.println("=========================updatePath"); if (this->_updatePath != nullptr) { String pitem = htmlFormatProvider->getUpdate(); @@ -325,7 +319,6 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) } // -- Fill config version string; - Serial.println("=========================Fill config version string"); { String pitem = htmlFormatProvider->getConfigVer(); pitem.replace("{v}", this->_configVersion); From 16817664cdb54d983c360acec9c610bcfc7cd71a Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 2 Oct 2024 06:19:24 +0200 Subject: [PATCH 11/30] do not render parameters --- src/IotWebConf.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 61935d7..88e9aec 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -301,9 +301,9 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) webRequestWrapper->sendContent(content); #ifdef IOTWEBCONF_DEBUG_TO_SERIAL - Serial.println("Rendering parameters:"); - this->_systemParameters.debugTo(&Serial); - this->_customParameterGroups.debugTo(&Serial); + //Serial.println("Rendering parameters:"); + //this->_systemParameters.debugTo(&Serial); + //this->_customParameterGroups.debugTo(&Serial); #endif // -- Add parameters to the form this->_systemParameters.renderHtml(dataArrived, webRequestWrapper); From 745fdd40d4faac328b3b89f0c2437efae17241d8 Mon Sep 17 00:00:00 2001 From: Andreas Zogg Date: Wed, 13 Nov 2024 08:00:28 +0100 Subject: [PATCH 12/30] void setConfigSavedPage added --- src/IotWebConf.cpp | 12 +++++++++++- src/IotWebConf.h | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 88e9aec..011e67e 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -247,6 +247,11 @@ void IotWebConf::setConfigSavedCallback(std::function func) this->_configSavedCallback = func; } +void IotWebConf::setConfigSavedPage(std::function func){ + this->_configSavedPage = func; +} + + void IotWebConf::setFormValidator( std::function func) { @@ -369,7 +374,12 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) } else { - page += F("Return to home page."); + if (this->_configSavedPage != nullptr){ + this->_configSavedPage(webRequestWrapper); + return; + } else { + page += F("Return to home page."); + } } page += htmlFormatProvider->getEnd(); diff --git a/src/IotWebConf.h b/src/IotWebConf.h index 2f65e1c..51f899f 100644 --- a/src/IotWebConf.h +++ b/src/IotWebConf.h @@ -293,6 +293,8 @@ class IotWebConf */ void setConfigSavedCallback(std::function func); + void setConfigSavedPage(std::function func); + /** * Specify a callback method, that will be called when form validation is required. * If the method will return false, the configuration will not be saved. @@ -600,6 +602,7 @@ class IotWebConf std::function _wifiConnectionCallback = nullptr; std::function _configSavingCallback = nullptr; std::function _configSavedCallback = nullptr; + std::function _configSavedPage = nullptr; std::function _formValidator = nullptr; std::function _apConnectionHandler = &(IotWebConf::connectAp); From 6e266874f3f6a5d9d810b5aad3665d78431de4a6 Mon Sep 17 00:00:00 2001 From: Andreas Zogg Date: Wed, 13 Nov 2024 14:41:28 +0100 Subject: [PATCH 13/30] Update setConfigSavedPage to accept void return type for custom actions --- src/IotWebConf.cpp | 3 +-- src/IotWebConf.h | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 011e67e..1d2dda3 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -247,11 +247,10 @@ void IotWebConf::setConfigSavedCallback(std::function func) this->_configSavedCallback = func; } -void IotWebConf::setConfigSavedPage(std::function func){ +void IotWebConf::setConfigSavedPage(std::function func){ this->_configSavedPage = func; } - void IotWebConf::setFormValidator( std::function func) { diff --git a/src/IotWebConf.h b/src/IotWebConf.h index 51f899f..946b143 100644 --- a/src/IotWebConf.h +++ b/src/IotWebConf.h @@ -293,7 +293,14 @@ class IotWebConf */ void setConfigSavedCallback(std::function func); - void setConfigSavedPage(std::function func); + /** + * Sets a custom function that will be called when the configuration page is + * saved. This function can be used to perform custom actions after the + * configuration has been successfully saved. + * @func - A function that accepts a WebRequestWrapper as a parameter + */ + void setConfigSavedPage(std::function func); + /** * Specify a callback method, that will be called when form validation is required. @@ -602,7 +609,7 @@ class IotWebConf std::function _wifiConnectionCallback = nullptr; std::function _configSavingCallback = nullptr; std::function _configSavedCallback = nullptr; - std::function _configSavedPage = nullptr; + std::function _configSavedPage = nullptr; std::function _formValidator = nullptr; std::function _apConnectionHandler = &(IotWebConf::connectAp); From 3f8f6076d4a869be2631071a2dac67cb7fbdf378 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 15 Nov 2024 06:22:25 +0100 Subject: [PATCH 14/30] Add support for custom pages when configuration is incomplete --- src/IotWebConf.cpp | 69 ++++++++++++++++++++++++++++++++++++---------- src/IotWebConf.h | 12 ++++++++ 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 1d2dda3..3e01d23 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -247,7 +247,27 @@ void IotWebConf::setConfigSavedCallback(std::function func) this->_configSavedCallback = func; } -void IotWebConf::setConfigSavedPage(std::function func){ +void IotWebConf::setConfigAPPasswordMissingPage( + std::function func) +{ + this->_configAPPasswordMissingPage = func; +} + +void IotWebConf::setConfigSSIDNotConfiguredPage( + std::function func) +{ + this->_configSSIDNotConfiguredPage = func; +} + +void IotWebConf::setConfigNotConfiguredPage( + std::function func) +{ + this->_configNotConfiguredPage = func; +} + +void IotWebConf::setConfigSavedPage( + std::function func) +{ this->_configSavedPage = func; } @@ -359,26 +379,45 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) page += "Configuration saved. "; if (this->_apPassword[0] == '\0') { - page += F("You must change the default AP password to continue. Return " - "to configuration page."); - } - else if (this->_wifiParameters._wifiSsid[0] == '\0') - { - page += F("You must provide the local wifi settings to continue. Return " - "to configuration page."); + if (this->_configAPPasswordMissingPage != nullptr) + { + this->_configAPPasswordMissingPage(webRequestWrapper); + return; + } + else + { + page += F("You must change the default AP password to continue. Return " + "to configuration page."); + } } - else if (this->_state == NotConfigured) - { - page += F("Please disconnect from WiFi AP to continue!"); + else if (this->_wifiParameters._wifiSsid[0] == '\0') { + if (this->_configSSIDNotConfiguredPage != nullptr) + { + this->_configSSIDNotConfiguredPage(webRequestWrapper); + return; + } + else + { + page += F("You must provide the local wifi settings to continue. " + "Return to configuration page."); + } } - else + else if (this->_state == NotConfigured) { + if (this->_configNotConfiguredPage != nullptr) + { + this->_configNotConfiguredPage(webRequestWrapper); + return; + } else { + page += F("Please disconnect from WiFi AP to continue!"); + } + } + else { if (this->_configSavedPage != nullptr){ this->_configSavedPage(webRequestWrapper); return; - } else { - page += F("Return to home page."); - } + } + page += F("Return to home page."); } page += htmlFormatProvider->getEnd(); diff --git a/src/IotWebConf.h b/src/IotWebConf.h index 946b143..f58ed78 100644 --- a/src/IotWebConf.h +++ b/src/IotWebConf.h @@ -293,6 +293,15 @@ class IotWebConf */ void setConfigSavedCallback(std::function func); + void setConfigAPPasswordMissingPage( + std::function func); + + void setConfigSSIDNotConfiguredPage( + std::function func); + + void setConfigNotConfiguredPage( + std::function func); + /** * Sets a custom function that will be called when the configuration page is * saved. This function can be used to perform custom actions after the @@ -609,6 +618,9 @@ class IotWebConf std::function _wifiConnectionCallback = nullptr; std::function _configSavingCallback = nullptr; std::function _configSavedCallback = nullptr; + std::function _configSSIDNotConfiguredPage = nullptr; + std::function _configAPPasswordMissingPage = nullptr; + std::function _configNotConfiguredPage = nullptr; std::function _configSavedPage = nullptr; std::function _formValidator = nullptr; std::function _apConnectionHandler = From d09d9577ebe009e1baf4c8486116ded731d3cdce Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 15 Nov 2024 07:03:43 +0100 Subject: [PATCH 15/30] Update configuration header to include device name --- library.properties | 2 +- src/IotWebConf.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library.properties b/library.properties index 67dbf36..0399a21 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.2.1 +version=3.2.3 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 3e01d23..6dc5f96 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -314,7 +314,7 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) webRequestWrapper->send(200, "text/html; charset=UTF-8", ""); String content = htmlFormatProvider->getHead(); - content.replace("{v}", "Config ESP"); + content.replace("{v}", String("Config ") + this->getThingName()); content += htmlFormatProvider->getScript(); content += htmlFormatProvider->getStyle(); content += htmlFormatProvider->getHeadExtension(); From 13a31f43f365455c3a485478770ce1295f2120e4 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 17 Nov 2024 08:34:44 +0100 Subject: [PATCH 16/30] Enhance firmware update HTML interface with improved styling and navigation links --- library.properties | 2 +- src/IotWebConfESP32HTTPUpdateServer.h | 47 ++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/library.properties b/library.properties index 0399a21..7b19fef 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.2.3 +version=3.2.4 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConfESP32HTTPUpdateServer.h b/src/IotWebConfESP32HTTPUpdateServer.h index b1357bf..cabcfe2 100644 --- a/src/IotWebConfESP32HTTPUpdateServer.h +++ b/src/IotWebConfESP32HTTPUpdateServer.h @@ -152,14 +152,45 @@ class HTTPUpdateServer String _password; bool _authenticated; String _updaterError; - const char* serverIndex PROGMEM = -R"(
- - -
- )"; - const char* successResponse PROGMEM = -"Update Success! Rebooting...\n"; + const char* serverIndex PROGMEM =R"( + + + + + +
+ + + +
+
+
+ Firmware update +
+ + +
+
+
+ + + +
Go to configure page to change configuration.
Go to main page.
+ + )"; + const char* successResponse PROGMEM = + "Update Success! " + "Rebooting...\n"; }; ///////////////////////////////////////////////////////////////////////////////// From d2406ebe0fad6c9548d47ba0e6b6a918eaaaa461 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 11 Jul 2025 06:43:41 +0200 Subject: [PATCH 17/30] Add detailed documentation for input parameters and their types --- README.md | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/README.md b/README.md index 70ee741..92ef546 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,237 @@ to ESP32. There are two major problems. - ESP8266 uses specific naming for it's classes (e.g. ESP8266WebServer). However, ESP32 uses a more generic naming (e.g. WebServer). The idea here is to use the generic naming hoping that ESP8266 will adopt these "standards" sooner or later. - ESP32 does not provide an HTTPUpdateServer implementation. So in this project we have implemented one. Whenever ESP32 provides an official HTTPUpdateServer, this local implementation will be removed. +## Input parameters + +IotWebConf provides various parameter types for creating configuration forms. Each parameter type is designed for specific use cases and renders appropriate HTML input fields. + +### Base Classes + +#### ConfigItem +Abstract base class for all configuration items. Provides core functionality for: +- EEPROM storage and loading +- HTML rendering +- Value validation +- Debug output +- JSON serialization (if enabled) + +#### ParameterGroup +Groups related parameters together in fieldsets for better organization. + +```cpp +ParameterGroup group("groupId", "Group Label"); +``` + +#### Parameter +Base class for all input parameters with common properties: +- `label` - Display label in the configuration form +- `id` - Unique identifier for HTML and storage +- `valueBuffer` - Buffer to store the parameter value +- `length` - Buffer length +- `defaultValue` - Default value on first startup +- `errorMessage` - Validation error message +- `visible` - Controls parameter visibility + +### Available Parameter Types + +#### TextParameter +Standard text input field for string values. + +```cpp +TextParameter textParam( + "Parameter Label", // label + "paramId", // id + valueBuffer, // value buffer + 32, // buffer length + "default value", // default value (optional) + "placeholder text", // placeholder (optional) + "custom HTML attrs" // custom HTML attributes (optional) +); +``` + +**Properties:** +- `placeholder` - Text displayed in empty input field +- `customHtml` - Additional HTML attributes for customization + +#### PasswordParameter +Password input field with special handling - only updates EEPROM when a new value is provided. + +```cpp +PasswordParameter passwordParam( + "Password", + "password", + passwordBuffer, + 32, + "defaultPass", // default value (optional) + "Enter password", // placeholder (optional) + "ondblclick=\"pw(this.id)\"" // custom HTML (optional) +); +``` + +**Features:** +- Masked input field (type="password") +- Double-click to reveal password (with default customHtml) +- Only saves to EEPROM when new value provided +- Limited to IOTWEBCONF_PASSWORD_LEN characters + +#### NumberParameter +Numeric input field with number validation. + +```cpp +NumberParameter numberParam( + "Port Number", + "port", + portBuffer, + 6, + "8080", // default value (optional) + "Enter port", // placeholder (optional) + "min='1' max='65535'" // custom HTML for validation (optional) +); +``` + +**Features:** +- HTML5 number input type +- Browser-native numeric validation +- Supports min/max attributes via customHtml + +#### CheckboxParameter +Checkbox input for boolean values. + +```cpp +CheckboxParameter checkboxParam( + "Enable Feature", + "enable", + enableBuffer, + 10, + true // default checked state +); +``` + +**Features:** +- Value is either empty or "selected" +- `isChecked()` method returns boolean state +- Special form handling (unchecked boxes don't send values) + +#### SelectParameter +Dropdown selection from predefined options. + +```cpp +// Define options +const char* optionValues = "opt1\0opt2\0opt3"; +const char* optionNames = "Option 1\0Option 2\0Option 3"; + +SelectParameter selectParam( + "Select Option", + "option", + optionBuffer, + 10, + optionValues, // values to store + optionNames, // display names + 3, // option count + 20, // max name length + "opt1" // default value (optional) +); +``` + +**Features:** +- Dropdown HTML SELECT element +- Separate values and display names +- Configurable option count and name length + +#### DateParameter +HTML5 date picker input. + +```cpp +DateParameter dateParam( + "Select Date", + "date", + dateBuffer, + 12, + "2023-01-01" // default value (optional) +); +``` + +**Features:** +- Native browser date picker +- ISO date format (YYYY-MM-DD) +- Calendar popup interface + +#### TimeParameter +HTML5 time picker input. + +```cpp +TimeParameter timeParam( + "Select Time", + "time", + timeBuffer, + 8, + "12:00" // default value (optional) +); +``` + +**Features:** +- Native browser time picker +- 24-hour format (HH:MM) +- Time selection interface + +### Parameter Organization + +#### Adding Parameters to Groups +```cpp +ParameterGroup mainGroup("main", "Main Settings"); +mainGroup.addItem(&textParam); +mainGroup.addItem(&passwordParam); +mainGroup.addItem(&numberParam); +``` + +#### Visibility Control +```cpp +// Hide parameter conditionally +textParam.visible = false; +``` + +### Advanced Features + +#### Custom HTML Attributes +All text-based parameters support custom HTML attributes: +```cpp +TextParameter customParam( + "Custom Field", + "custom", + buffer, + 32, + nullptr, + "placeholder", + "required pattern='[A-Za-z]+' title='Letters only'" +); +``` + +#### Validation +Parameters can have error messages set for validation feedback: +```cpp +if (strlen(textParam.valueBuffer) < 3) { + textParam.errorMessage = "Minimum 3 characters required"; +} +``` + +#### JSON Support +When `IOTWEBCONF_ENABLE_JSON` is defined, parameters support JSON serialization: +```cpp +#ifdef IOTWEBCONF_ENABLE_JSON + parameter.loadFromJson(jsonObject); +#endif +``` + +### Best Practices + +1. **Buffer Management**: Always ensure value buffers are large enough for expected values +2. **Parameter IDs**: Use unique, descriptive IDs without spaces or special characters +3. **Default Values**: Provide sensible defaults for better user experience +4. **Validation**: Implement proper validation and error handling +5. **Organization**: Group related parameters together using ParameterGroup +6. **Security**: Use PasswordParameter for sensitive data + + ## Customizing and extending functionality IotWebConf is ment to be developer friendly by providing lots of customization options. See [HackingGuide](doc/HackingGuide.md) for From 80cffe388dc0c121c07ff5b00ffd856661f3f6a5 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 21 Aug 2025 07:04:11 +0200 Subject: [PATCH 18/30] Remove debug serial prints in handleCaptivePortal function --- src/IotWebConf.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 46d973c..05bfcc3 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -508,12 +508,12 @@ bool IotWebConf::handleCaptivePortal(WebRequestWrapper* webRequestWrapper) if (!isIp(host) && !host.startsWith(thingName)) { #ifdef IOTWEBCONF_DEBUG_TO_SERIAL - Serial.print("Request for "); - Serial.print(host); - Serial.print(" redirected to "); - Serial.print(webRequestWrapper->localIP()); - Serial.print(":"); - Serial.println(webRequestWrapper->localPort()); + //Serial.print("Request for "); + //Serial.print(host); + //Serial.print(" redirected to "); + //Serial.print(webRequestWrapper->localIP()); + //Serial.print(":"); + //Serial.println(webRequestWrapper->localPort()); #endif webRequestWrapper->sendHeader( "Location", String("http://") + toStringIp(webRequestWrapper->localIP()) + ":" + webRequestWrapper->localPort(), true); From 9fc8d68110fd0f7def37128029eaae0f3583cbb2 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 7 Nov 2025 15:54:15 +0100 Subject: [PATCH 19/30] Make isActive and setActive methods virtual in OptionalParameterGroup class --- library.properties | 2 +- src/IotWebConfOptionalGroup.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library.properties b/library.properties index 7b19fef..375e9a7 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.2.4 +version=3.2.5 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConfOptionalGroup.h b/src/IotWebConfOptionalGroup.h index 60a2b1f..d7aeb71 100644 --- a/src/IotWebConfOptionalGroup.h +++ b/src/IotWebConfOptionalGroup.h @@ -71,8 +71,8 @@ class OptionalParameterGroup : public ParameterGroup { public: OptionalParameterGroup(const char* id, const char* label, bool defaultActive); - bool isActive() { return this->_active; } - void setActive(bool active) { this->_active = active; } + virtual bool isActive() const { return this->_active; } + virtual void setActive(bool active) { this->_active = active; } protected: int getStorageSize() override; From 6d6ee4718d0d267c366995b52d4b7aa783401adb Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 23 Nov 2025 09:54:31 +0100 Subject: [PATCH 20/30] file removed --- .clang-format | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .clang-format diff --git a/.clang-format b/.clang-format deleted file mode 100644 index c2b03d8..0000000 --- a/.clang-format +++ /dev/null @@ -1,5 +0,0 @@ -BasedOnStyle: LLVM -PointerAlignment: Left -AllowShortFunctionsOnASingleLine: InlineOnly -AlignAfterOpenBracket: AlwaysBreak -BreakBeforeBraces: Allman \ No newline at end of file From 073e21219bf557e625bfe8e61828e8bce382e6b3 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 23 Nov 2025 10:06:55 +0100 Subject: [PATCH 21/30] feat: Add support for real chunking with ESPAsyncWebServer Extended the library to support proper chunked response handling with ESPAsyncWebServer, enabling efficient streaming of large responses. --- library.properties | 2 +- src/IotWebConf.cpp | 30 +++++++++------- src/IotWebConf.h | 17 ++++++++- src/IotWebConfOptionalGroup.cpp | 62 +++++++++++++++++++++++++++++++-- src/IotWebConfOptionalGroup.h | 3 ++ src/IotWebConfParameter.cpp | 61 ++++++++++++++++++++++++++++++-- src/IotWebConfParameter.h | 20 +++++++---- src/IotWebConfTParameter.h | 11 ++++-- 8 files changed, 179 insertions(+), 27 deletions(-) diff --git a/library.properties b/library.properties index 375e9a7..aa42957 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.2.5 +version=3.3.0 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 05bfcc3..1bbea74 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -284,6 +284,21 @@ void IotWebConf::setWifiConnectionTimeoutMs(unsigned long millis) //////////////////////////////////////////////////////////////////////////////// +String IotWebConf::getUpdateLinkHtml() { + if (this->_updatePath != nullptr) { + String pitem = htmlFormatProvider->getUpdate(); + pitem.replace("{u}", this->_updatePath); + return pitem; + } + return String(); +} + +String IotWebConf::getConfigVersionHtml() { + String pitem = htmlFormatProvider->getConfigVer(); + pitem.replace("{v}", this->_configVersion); + return pitem; +} + void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) { if (this->_state == OnLine) @@ -335,19 +350,8 @@ void IotWebConf::handleConfig(WebRequestWrapper* webRequestWrapper) content = htmlFormatProvider->getFormEnd(); - if (this->_updatePath != nullptr) - { - String pitem = htmlFormatProvider->getUpdate(); - pitem.replace("{u}", this->_updatePath); - content += pitem; - } - - // -- Fill config version string; - { - String pitem = htmlFormatProvider->getConfigVer(); - pitem.replace("{v}", this->_configVersion); - content += pitem; - } + content += this->getUpdateLinkHtml(); + content += this->getConfigVersionHtml(); content += htmlFormatProvider->getEnd(); diff --git a/src/IotWebConf.h b/src/IotWebConf.h index 4d0d293..27f5a62 100644 --- a/src/IotWebConf.h +++ b/src/IotWebConf.h @@ -523,6 +523,11 @@ class IotWebConf { return &this->_systemParameters; }; + + ParameterGroup* getCustomParameterGroup() { + return &this->_customParameterGroups; + } + Parameter* getThingNameParameter() { return &this->_thingNameParameter; @@ -548,6 +553,14 @@ class IotWebConf return &this->_apTimeoutParameter; }; + const char* getApPassword() const { + return this->_apPassword; + } + + String getUpdateLinkHtml(); + String getConfigVersionHtml(); + + /** * If config parameters are modified directly, the new values can be saved by this method. * Note, that init() must pretend saveConfig()! @@ -576,6 +589,8 @@ class IotWebConf return this->htmlFormatProvider; } + bool validateForm(WebRequestWrapper* webRequestWrapper); + private: const char* _initialApPassword = nullptr; const char* _configVersion; @@ -648,7 +663,7 @@ class IotWebConf void readEepromValue(int start, byte* valueBuffer, int length); void writeEepromValue(int start, byte* valueBuffer, int length); - bool validateForm(WebRequestWrapper* webRequestWrapper); + void changeState(NetworkState newState); void stateChanged(NetworkState oldState, NetworkState newState); diff --git a/src/IotWebConfOptionalGroup.cpp b/src/IotWebConfOptionalGroup.cpp index 9e64649..c1f210d 100644 --- a/src/IotWebConfOptionalGroup.cpp +++ b/src/IotWebConfOptionalGroup.cpp @@ -61,8 +61,7 @@ void OptionalParameterGroup::loadValue( ParameterGroup::loadValue(doLoad); } -void OptionalParameterGroup::renderHtml( - bool dataArrived, WebRequestWrapper* webRequestWrapper) +void OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) { if (this->label != nullptr) { @@ -100,6 +99,65 @@ void OptionalParameterGroup::renderHtml( } } +bool OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { + // Serial.println("OptionalParameterGroup::renderHtml called"); + + ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; + + if (!_startTemplateSend && this->label != nullptr) { + //Serial.println(" outputting start template"); + String content_ = getStartTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + content_.replace("{v}", this->_active ? "active" : "inactive"); + if (this->_active) { + content_.replace("{cb}", "hide"); + content_.replace("{cf}", ""); + } + else { + content_.replace("{cb}", ""); + content_.replace("{cf}", "hide"); + } + bool completed_ = outputCallback(content_.c_str(), content_.length()); + if (!completed_) { + //Serial.println("Rendering interrupted during start template, saving state."); + return completed_; + } + _startTemplateSend = true; + } + + + while (current_ != nullptr) { + //Serial.print(" rendering item: "); Serial.println(current_->getId()); + if (current_->visible) { + bool completed_ = current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); + if (!completed_) { + //Serial.println("Rendering interrupted, saving state."); + _currentItem = current_; + return completed_; + } + } + current_ = this->getNextItemOf(current_); + } + + if (this->label != nullptr) { + //Serial.println(" outputting end template"); + String content_ = getEndTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + bool completed_ = outputCallback(content_.c_str(), content_.length()); + if (!completed_) { + //Serial.println("Rendering interrupted during end template, saving state."); + return completed_; + } + } + + _currentItem = nullptr; + _startTemplateSend = false; + //Serial.println("OptionalParameterGroup::renderHtml completed"); + return true; +} + void OptionalParameterGroup::update(WebRequestWrapper* webRequestWrapper) { // -- Get active variable diff --git a/src/IotWebConfOptionalGroup.h b/src/IotWebConfOptionalGroup.h index d7aeb71..74f6a28 100644 --- a/src/IotWebConfOptionalGroup.h +++ b/src/IotWebConfOptionalGroup.h @@ -82,6 +82,7 @@ class OptionalParameterGroup : public ParameterGroup void loadValue(std::function doLoad) override; void renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) override; + bool renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) override; virtual String getStartTemplate() { return FPSTR(IOTWEBCONF_HTML_FORM_OPTIONAL_GROUP_START); }; virtual String getEndTemplate() { return FPSTR(IOTWEBCONF_HTML_FORM_OPTIONAL_GROUP_END); }; void update(WebRequestWrapper* webRequestWrapper) override; @@ -90,6 +91,8 @@ class OptionalParameterGroup : public ParameterGroup private: bool _defaultActive; bool _active; + bool _continueRendering = false; + }; class ChainedParameterGroup; diff --git a/src/IotWebConfParameter.cpp b/src/IotWebConfParameter.cpp index b41cff3..baa677f 100644 --- a/src/IotWebConfParameter.cpp +++ b/src/IotWebConfParameter.cpp @@ -110,6 +110,55 @@ void ParameterGroup::renderHtml( webRequestWrapper->sendContent(content); } } + +bool ParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { + //Serial.println("ParameterGroup::renderHtml called"); + + ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; + + if (!_startTemplateSend && this->label != nullptr) { + //Serial.println("Sending start template"); + String content_ = getStartTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + bool completed_ = outputCallback(content_.c_str(), content_.length()); + if (!completed_) { + //Serial.println("Rendering interrupted during start template, saving state."); + return completed_; + } + _startTemplateSend = true; + } + + while (current_ != nullptr) { + //Serial.print("Rendering item: "); Serial.println(current_->getId()); + if (current_->visible) { + bool completed_ = current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); + if (!completed_) { + //Serial.println("Rendering interrupted, saving state."); + _currentItem = current_; + return completed_; + } + } + current_ = current_->_nextItem; + } + + if (this->label != nullptr) { + String content_ = getEndTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + bool completed_ = outputCallback(content_.c_str(), content_.length()); + if (!completed_) { + //Serial.println("Rendering interrupted during end template, saving state."); + return completed_; + } + } + + _currentItem = nullptr; + _startTemplateSend = false; + //Serial.println("ParameterGroup::renderHtml completed"); + return true; +} + void ParameterGroup::update(WebRequestWrapper* webRequestWrapper) { ConfigItem* current = this->_firstItem; @@ -295,8 +344,7 @@ TextParameter::TextParameter( this->customHtml = customHtml; } -void TextParameter::renderHtml( - bool dataArrived, WebRequestWrapper* webRequestWrapper) +void TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) { String content = this->renderHtml( dataArrived, @@ -304,6 +352,15 @@ void TextParameter::renderHtml( webRequestWrapper->arg(this->getId())); webRequestWrapper->sendContent(content); } + +bool TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { + String content = this->renderHtml( + dataArrived, + webRequestWrapper->hasArg(this->getId()), + webRequestWrapper->arg(this->getId())); + return outputCallback(content.c_str(), content.length()); +} + String TextParameter::renderHtml( bool dataArrived, bool hasValueFromPost, String valueFromPost) { diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index b5cf265..1238918 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -38,6 +38,8 @@ const char IOTWEBCONF_HTML_FORM_SELECT_PARAM[] PROGMEM = const char IOTWEBCONF_HTML_FORM_OPTION[] PROGMEM = "\n"; +typedef std::function HtmlChunkCallback; + namespace iotwebconf { @@ -90,7 +92,7 @@ class ConfigItem * The webRequestWrapper->sendContent() method should be used in the implementations. */ virtual void renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) = 0; - + virtual bool renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) = 0; /** * New value arrived from the form post. The value should be stored in the * in this config item. @@ -141,13 +143,16 @@ class ParameterGroup : public ConfigItem virtual void loadFromJson(JsonObject jsonObject) override; #endif + void renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) override; + bool renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) override; + protected: int getStorageSize() override; void storeValue(std::function doStore) override; void loadValue(std::function doLoad) override; - void renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) override; + void update(WebRequestWrapper* webRequestWrapper) override; void clearErrorMessage() override; void debugTo(Stream* out) override; @@ -163,6 +168,10 @@ class ParameterGroup : public ConfigItem virtual String getEndTemplate() { return FPSTR(IOTWEBCONF_HTML_FORM_GROUP_END); }; ConfigItem* _firstItem = nullptr; + ConfigItem* _currentItem = nullptr; + bool _startTemplateSend = false; + bool _continueRendering = false; + size_t _currentStringPos = 0; ConfigItem* getNextItemOf(ConfigItem* parent) { return parent->_nextItem; }; friend class IotWebConf; // Allow IotWebConf to access protected members. @@ -253,10 +262,10 @@ class TextParameter : public Parameter const char* customHtml; protected: - virtual String renderHtml( - bool dataArrived, bool hasValueFromPost, String valueFromPost); + virtual String renderHtml(bool dataArrived, bool hasValueFromPost, String valueFromPost); // Overrides virtual void renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) override; + virtual bool renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) override; virtual void update(String newValue) override; virtual void debugTo(Stream* out) override; /** @@ -478,8 +487,7 @@ class TimeParameter : public TextParameter { } protected: - virtual String renderHtml( - bool dataArrived, bool hasValueFromPost, String valueFromPost) override + virtual String renderHtml(bool dataArrived, bool hasValueFromPost, String valueFromPost) override { return TextParameter::renderHtml("time", hasValueFromPost, valueFromPost); }; diff --git a/src/IotWebConfTParameter.h b/src/IotWebConfTParameter.h index aab28d2..077a28b 100644 --- a/src/IotWebConfTParameter.h +++ b/src/IotWebConfTParameter.h @@ -385,8 +385,7 @@ class InputParameter : virtual public ConfigItemBridge ConfigItemBridge::ConfigItemBridge(id), label(label) { } - virtual void renderHtml( - bool dataArrived, WebRequestWrapper* webRequestWrapper) override + virtual void renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper) override { String content = this->renderHtml( dataArrived, @@ -395,6 +394,14 @@ class InputParameter : virtual public ConfigItemBridge webRequestWrapper->sendContent(content); } + virtual bool renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) override { + String content = this->renderHtml( + dataArrived, + webRequestWrapper->hasArg(this->getId()), + webRequestWrapper->arg(this->getId())); + return outputCallback(content.c_str(), content.length()); + } + const char* label; /** From ab9b69d65f3850f9151eb162bb9478c9b423dd49 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 3 Jan 2026 07:09:32 +0100 Subject: [PATCH 22/30] fix: handle template sending state --- src/IotWebConfOptionalGroup.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/IotWebConfOptionalGroup.cpp b/src/IotWebConfOptionalGroup.cpp index c1f210d..8049f40 100644 --- a/src/IotWebConfOptionalGroup.cpp +++ b/src/IotWebConfOptionalGroup.cpp @@ -100,7 +100,11 @@ void OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* web } bool OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { - // Serial.println("OptionalParameterGroup::renderHtml called"); + //Serial.println("OptionalParameterGroup::renderHtml called"); + + if (!dataArrived && _currentItem == nullptr && _startTemplateSend) { + _startTemplateSend = false; + } ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; @@ -128,11 +132,12 @@ bool OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* web while (current_ != nullptr) { - //Serial.print(" rendering item: "); Serial.println(current_->getId()); + Serial.print(" rendering item: "); Serial.println(current_->getId()); if (current_->visible) { bool completed_ = current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); if (!completed_) { - //Serial.println("Rendering interrupted, saving state."); + Serial.println("Rendering interrupted, saving state."); + Serial.print(" Completed: "); Serial.println(completed_ ? "true" : "false"); _currentItem = current_; return completed_; } From 9a836a6da5594258688e5384c71d02e12fc9f4fa Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 3 Jan 2026 07:12:37 +0100 Subject: [PATCH 23/30] fix: handle template sending state --- library.properties | 2 +- src/IotWebConfParameter.cpp | 13 ++++++++++++- src/IotWebConfParameter.h | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/library.properties b/library.properties index aa42957..b22975c 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.3.0 +version=3.3.1 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConfParameter.cpp b/src/IotWebConfParameter.cpp index baa677f..bf461cb 100644 --- a/src/IotWebConfParameter.cpp +++ b/src/IotWebConfParameter.cpp @@ -358,7 +358,18 @@ bool TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWr dataArrived, webRequestWrapper->hasArg(this->getId()), webRequestWrapper->arg(this->getId())); - return outputCallback(content.c_str(), content.length()); + size_t contentLen = content.length(); + while (_lastSentPos < contentLen) { + size_t toSend = contentLen - _lastSentPos; + if (!outputCallback(content.c_str() + _lastSentPos, toSend)) { + // Buffer full, save progress + _lastSentPos += toSend; + return false; + } + _lastSentPos += toSend; + } + _lastSentPos = 0; // Reset after complete send + return true; } String TextParameter::renderHtml( diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index 1238918..e92cba5 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -280,6 +280,8 @@ class TextParameter : public Parameter */ virtual String renderHtml(const char* type, bool hasValueFromPost, String valueFromPost); + size_t _lastSentPos = 0; + private: friend class IotWebConf; friend class WifiParameterGroup; From 8f415be7b7cdf08f11e26af8665363a57626e52a Mon Sep 17 00:00:00 2001 From: Andreas Zogg Date: Fri, 9 Jan 2026 13:16:23 +0100 Subject: [PATCH 24/30] removed file --- .clang-format | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c2b03d8 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: LLVM +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: InlineOnly +AlignAfterOpenBracket: AlwaysBreak +BreakBeforeBraces: Allman \ No newline at end of file From 82192874a7ab9c4cba131e52388130879fd8787e Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 17 Jan 2026 16:28:58 +0100 Subject: [PATCH 25/30] Fix async rendering: prevent form elements from being skipped when buffer is full Fixed TextParameter::renderHtml() to only update position tracker on successful write, preventing elements like checkboxes from being skipped during chunked HTTP responses. --- library.properties | 2 +- src/IotWebConfOptionalGroup.cpp | 12 +++++------ src/IotWebConfParameter.cpp | 38 ++++++++++++++++++++------------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/library.properties b/library.properties index b22975c..b17e96f 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.3.1 +version=3.3.2 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConfOptionalGroup.cpp b/src/IotWebConfOptionalGroup.cpp index 8049f40..c2591e3 100644 --- a/src/IotWebConfOptionalGroup.cpp +++ b/src/IotWebConfOptionalGroup.cpp @@ -102,12 +102,12 @@ void OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* web bool OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { //Serial.println("OptionalParameterGroup::renderHtml called"); - if (!dataArrived && _currentItem == nullptr && _startTemplateSend) { + ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; + + if (_currentItem == nullptr) { _startTemplateSend = false; } - ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; - if (!_startTemplateSend && this->label != nullptr) { //Serial.println(" outputting start template"); String content_ = getStartTemplate(); @@ -132,12 +132,12 @@ bool OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* web while (current_ != nullptr) { - Serial.print(" rendering item: "); Serial.println(current_->getId()); + //Serial.print(" rendering item: "); Serial.println(current_->getId()); if (current_->visible) { bool completed_ = current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); if (!completed_) { - Serial.println("Rendering interrupted, saving state."); - Serial.print(" Completed: "); Serial.println(completed_ ? "true" : "false"); + //Serial.println("Rendering interrupted, saving state."); + //Serial.print(" Completed: "); Serial.println(completed_ ? "true" : "false"); _currentItem = current_; return completed_; } diff --git a/src/IotWebConfParameter.cpp b/src/IotWebConfParameter.cpp index bf461cb..9818f21 100644 --- a/src/IotWebConfParameter.cpp +++ b/src/IotWebConfParameter.cpp @@ -116,6 +116,10 @@ bool ParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestW ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; + if (_currentItem == nullptr){ + _startTemplateSend = false; + } + if (!_startTemplateSend && this->label != nullptr) { //Serial.println("Sending start template"); String content_ = getStartTemplate(); @@ -139,7 +143,7 @@ bool ParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestW return completed_; } } - current_ = current_->_nextItem; + current_ = this->getNextItemOf(current_); } if (this->label != nullptr) { @@ -353,23 +357,27 @@ void TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWr webRequestWrapper->sendContent(content); } -bool TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { - String content = this->renderHtml( - dataArrived, - webRequestWrapper->hasArg(this->getId()), - webRequestWrapper->arg(this->getId())); - size_t contentLen = content.length(); - while (_lastSentPos < contentLen) { - size_t toSend = contentLen - _lastSentPos; - if (!outputCallback(content.c_str() + _lastSentPos, toSend)) { - // Buffer full, save progress - _lastSentPos += toSend; - return false; - } - _lastSentPos += toSend; +bool TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback){ + String content = this->renderHtml(dataArrived, webRequestWrapper->hasArg(this->getId()), webRequestWrapper->arg(this->getId())); + size_t contentLen = content.length(); + + if (_lastSentPos < contentLen){ + size_t toSend = contentLen - _lastSentPos; + bool success = outputCallback(content.c_str() + _lastSentPos, toSend); + if (!success){ + // Buffer full, could not write anything - position stays the same! + return false; } + // Successfully written - update position + _lastSentPos += toSend; + } + + if (_lastSentPos >= contentLen){ _lastSentPos = 0; // Reset after complete send return true; + } + + return false; // Not finished yet } String TextParameter::renderHtml( From 322d8b3bad92795490c8d76d43662ff81031190b Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 24 Jan 2026 07:39:32 +0100 Subject: [PATCH 26/30] fix: update HtmlChunkCallback to return size_t for better handling of output --- library.properties | 2 +- src/IotWebConfOptionalGroup.cpp | 109 +++++++++++++++--------------- src/IotWebConfParameter.cpp | 115 +++++++++++++++++--------------- src/IotWebConfParameter.h | 2 +- 4 files changed, 119 insertions(+), 109 deletions(-) diff --git a/library.properties b/library.properties index b17e96f..0742837 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.3.2 +version=3.3.3 author=Balazs Kelemen maintainer=Balazs Kelemen sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConfOptionalGroup.cpp b/src/IotWebConfOptionalGroup.cpp index c2591e3..6f52986 100644 --- a/src/IotWebConfOptionalGroup.cpp +++ b/src/IotWebConfOptionalGroup.cpp @@ -99,68 +99,71 @@ void OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* web } } -bool OptionalParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { - //Serial.println("OptionalParameterGroup::renderHtml called"); +bool OptionalParameterGroup::renderHtml( + bool dataArrived, WebRequestWrapper* webRequestWrapper, + HtmlChunkCallback outputCallback) +{ + ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; - ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; + if (_currentItem == nullptr) + { + _startTemplateSend = false; + } - if (_currentItem == nullptr) { - _startTemplateSend = false; + if (!_startTemplateSend && this->label != nullptr) + { + String content_ = getStartTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + content_.replace("{v}", this->_active ? "active" : "inactive"); + if (this->_active) + { + content_.replace("{cb}", "hide"); + content_.replace("{cf}", ""); } - - if (!_startTemplateSend && this->label != nullptr) { - //Serial.println(" outputting start template"); - String content_ = getStartTemplate(); - content_.replace("{b}", this->label); - content_.replace("{i}", this->getId()); - content_.replace("{v}", this->_active ? "active" : "inactive"); - if (this->_active) { - content_.replace("{cb}", "hide"); - content_.replace("{cf}", ""); - } - else { - content_.replace("{cb}", ""); - content_.replace("{cf}", "hide"); - } - bool completed_ = outputCallback(content_.c_str(), content_.length()); - if (!completed_) { - //Serial.println("Rendering interrupted during start template, saving state."); - return completed_; - } - _startTemplateSend = true; + else + { + content_.replace("{cb}", ""); + content_.replace("{cf}", "hide"); + } + size_t written = outputCallback(content_.c_str(), content_.length()); + if (written < content_.length()) + { + return false; } + _startTemplateSend = true; + } - - while (current_ != nullptr) { - //Serial.print(" rendering item: "); Serial.println(current_->getId()); - if (current_->visible) { - bool completed_ = current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); - if (!completed_) { - //Serial.println("Rendering interrupted, saving state."); - //Serial.print(" Completed: "); Serial.println(completed_ ? "true" : "false"); - _currentItem = current_; - return completed_; - } - } - current_ = this->getNextItemOf(current_); + while (current_ != nullptr) + { + if (current_->visible) + { + bool completed_ = + current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); + if (!completed_) + { + _currentItem = current_; + return false; + } } + current_ = this->getNextItemOf(current_); + } - if (this->label != nullptr) { - //Serial.println(" outputting end template"); - String content_ = getEndTemplate(); - content_.replace("{b}", this->label); - content_.replace("{i}", this->getId()); - bool completed_ = outputCallback(content_.c_str(), content_.length()); - if (!completed_) { - //Serial.println("Rendering interrupted during end template, saving state."); - return completed_; - } + if (this->label != nullptr) + { + String content_ = getEndTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + size_t written = outputCallback(content_.c_str(), content_.length()); + if (written < content_.length()) + { + return false; } + } - _currentItem = nullptr; - _startTemplateSend = false; - //Serial.println("OptionalParameterGroup::renderHtml completed"); - return true; + _currentItem = nullptr; + _startTemplateSend = false; + return true; } void OptionalParameterGroup::update(WebRequestWrapper* webRequestWrapper) diff --git a/src/IotWebConfParameter.cpp b/src/IotWebConfParameter.cpp index 9818f21..56a0d2b 100644 --- a/src/IotWebConfParameter.cpp +++ b/src/IotWebConfParameter.cpp @@ -111,56 +111,61 @@ void ParameterGroup::renderHtml( } } -bool ParameterGroup::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWrapper, HtmlChunkCallback outputCallback) { - //Serial.println("ParameterGroup::renderHtml called"); - - ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; +bool ParameterGroup::renderHtml( + bool dataArrived, WebRequestWrapper* webRequestWrapper, + HtmlChunkCallback outputCallback) +{ + ConfigItem* current_ = _currentItem ? _currentItem : this->_firstItem; - if (_currentItem == nullptr){ - _startTemplateSend = false; - } + if (_currentItem == nullptr) + { + _startTemplateSend = false; + } - if (!_startTemplateSend && this->label != nullptr) { - //Serial.println("Sending start template"); - String content_ = getStartTemplate(); - content_.replace("{b}", this->label); - content_.replace("{i}", this->getId()); - bool completed_ = outputCallback(content_.c_str(), content_.length()); - if (!completed_) { - //Serial.println("Rendering interrupted during start template, saving state."); - return completed_; - } - _startTemplateSend = true; + if (!_startTemplateSend && this->label != nullptr) + { + String content_ = getStartTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + size_t written = outputCallback(content_.c_str(), content_.length()); + if (written < content_.length()) + { + // Not everything was written + return false; } + _startTemplateSend = true; + } - while (current_ != nullptr) { - //Serial.print("Rendering item: "); Serial.println(current_->getId()); - if (current_->visible) { - bool completed_ = current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); - if (!completed_) { - //Serial.println("Rendering interrupted, saving state."); - _currentItem = current_; - return completed_; - } - } - current_ = this->getNextItemOf(current_); + while (current_ != nullptr) + { + if (current_->visible) + { + bool completed_ = + current_->renderHtml(dataArrived, webRequestWrapper, outputCallback); + if (!completed_) + { + _currentItem = current_; + return false; + } } + current_ = this->getNextItemOf(current_); + } - if (this->label != nullptr) { - String content_ = getEndTemplate(); - content_.replace("{b}", this->label); - content_.replace("{i}", this->getId()); - bool completed_ = outputCallback(content_.c_str(), content_.length()); - if (!completed_) { - //Serial.println("Rendering interrupted during end template, saving state."); - return completed_; - } + if (this->label != nullptr) + { + String content_ = getEndTemplate(); + content_.replace("{b}", this->label); + content_.replace("{i}", this->getId()); + size_t written = outputCallback(content_.c_str(), content_.length()); + if (written < content_.length()) + { + return false; } + } - _currentItem = nullptr; - _startTemplateSend = false; - //Serial.println("ParameterGroup::renderHtml completed"); - return true; + _currentItem = nullptr; + _startTemplateSend = false; + return true; } void ParameterGroup::update(WebRequestWrapper* webRequestWrapper) @@ -361,23 +366,25 @@ bool TextParameter::renderHtml(bool dataArrived, WebRequestWrapper* webRequestWr String content = this->renderHtml(dataArrived, webRequestWrapper->hasArg(this->getId()), webRequestWrapper->arg(this->getId())); size_t contentLen = content.length(); - if (_lastSentPos < contentLen){ - size_t toSend = contentLen - _lastSentPos; - bool success = outputCallback(content.c_str() + _lastSentPos, toSend); - if (!success){ - // Buffer full, could not write anything - position stays the same! + while (_lastSentPos < contentLen){ + size_t remaining = contentLen - _lastSentPos; + + // Try to write the remaining data + size_t written = outputCallback(content.c_str() + _lastSentPos, remaining); + + if (written == 0){ + // Callback could not write anything - buffer is full + // Don't update position, will retry on next call return false; } - // Successfully written - update position - _lastSentPos += toSend; - } - if (_lastSentPos >= contentLen){ - _lastSentPos = 0; // Reset after complete send - return true; + // Update position with what was actually written + _lastSentPos += written; } - return false; // Not finished yet + // All data sent + _lastSentPos = 0; // Reset for next parameter + return true; } String TextParameter::renderHtml( diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index e92cba5..f24cd11 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -38,7 +38,7 @@ const char IOTWEBCONF_HTML_FORM_SELECT_PARAM[] PROGMEM = const char IOTWEBCONF_HTML_FORM_OPTION[] PROGMEM = "\n"; -typedef std::function HtmlChunkCallback; +typedef std::function HtmlChunkCallback; namespace iotwebconf { From bf38415cb268ccf03d7cef3c2f177e9bb11a4da1 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 21 Jul 2026 06:21:28 +0200 Subject: [PATCH 27/30] feat: add DayParameter for multi-select weekday picker with customizable labels --- README.md | 48 +++++++++ library.properties | 4 +- src/IotWebConfParameter.cpp | 203 ++++++++++++++++++++++++++++++++++++ src/IotWebConfParameter.h | 91 +++++++++++++++- 4 files changed, 343 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 92ef546..75bed10 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,54 @@ DateParameter dateParam( - ISO date format (YYYY-MM-DD) - Calendar popup interface +#### DayParameter +Multi-select weekday picker with checkboxes. + +```cpp +// 1. Default English (all instances) +char workDaysBuffer[32] = "1,2,3,4,5"; +DayParameter workDays("Working Days", "workdays", workDaysBuffer, 32); + +char weekendBuffer[32] = "6,7"; +DayParameter weekend("Weekend", "weekend", weekendBuffer, 32); + +// 2. Switch to German (applies to ALL DayParameter instances) +const char* germanDaysShort[7] = {"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"}; +DayParameter::setDayNamesShort(germanDaysShort); + +// Now both workDays AND weekend display German names + +// 3. Single-select mode +char backupDayBuffer[8] = "1"; +DayParameter backupDay("Backup Day", "backupday", backupDayBuffer, 8, "1", true); + +// 4. Runtime query and modification +// Check if a specific day is selected +if (workDays.isDaySelected(1)) { // Monday + Serial.println("Monday is selected"); +} + +// Get all selected days +int selectedDays[7]; +int count = workDays.getSelectedDays(selectedDays); +for (int i = 0; i < count; i++) { + Serial.print("Selected day: "); + Serial.println(selectedDays[i]); // 1-7 +} + +// Set selected days programmatically +int newDays[] = {1, 3, 5}; // Monday, Wednesday, Friday +workDays.setSelectedDays(newDays, 3); +``` + +**Features:** +- Multiple weekday selection with checkboxes +- Static day names (applies to all instances) +- Customizable day names via `setDayNamesShort()` +- Optional single-select mode (radio buttons) +- Days stored as comma-separated numbers (1=Mon, 7=Sun) +- Default English labels (Mon-Sun) + #### TimeParameter HTML5 time picker input. diff --git a/library.properties b/library.properties index 0742837..3c93e2a 100644 --- a/library.properties +++ b/library.properties @@ -1,7 +1,7 @@ name=IotWebConf -version=3.3.3 +version=3.4.0 author=Balazs Kelemen -maintainer=Balazs Kelemen +maintainer=Balazs Kelemen , Andreas Zogg sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. paragraph=IotWebConf will start up in AP (access point) mode, and provide a config portal for entering WiFi connection and other user-settings. The configuration is persisted in EEPROM. The config portal will stay available after WiFi connection was made. A WiFiManager alternative. category=Communication diff --git a/src/IotWebConfParameter.cpp b/src/IotWebConfParameter.cpp index 56a0d2b..f0f2a0f 100644 --- a/src/IotWebConfParameter.cpp +++ b/src/IotWebConfParameter.cpp @@ -704,4 +704,207 @@ size_t PrefixStreamWrapper::checkNewLine() return 0; } +/////////////////////////////////////////////////////////////////////////////// + +// Static day names initialization (English by default) +// Array index matches day of week: 0=Sunday, 1=Monday, ..., 6=Saturday +const char* DayParameter::_dayNamesShort[7] = {"Sun", "Mon", "Tue", "Wed", + "Thu", "Fri", "Sat"}; + +bool DayParameter::isDaySelected(int dayOfWeek) const +{ + if (dayOfWeek < 0 || dayOfWeek > 6) + return false; + + String value = String(valueBuffer); + String dayStr = String(dayOfWeek); + + // Check if day is in comma-separated list + int startPos = 0; + while (startPos < value.length()) + { + int commaPos = value.indexOf(',', startPos); + if (commaPos == -1) + commaPos = value.length(); + + String token = value.substring(startPos, commaPos); + token.trim(); + if (token == dayStr) + return true; + + startPos = commaPos + 1; + } + return false; +} + +int DayParameter::getSelectedDays(int* selectedDays) const +{ + int count = 0; + for (int day = 0; day <= 6; day++) + { + if (isDaySelected(day)) + { + selectedDays[count++] = day; + } + } + return count; +} + +void DayParameter::setSelectedDays(const int* selectedDays, int count) +{ + this->valueBuffer[0] = '\0'; + + for (int i = 0; i < count && i < 7; i++) + { + if (i > 0) + strcat(this->valueBuffer, ","); + char dayStr[4]; + itoa(selectedDays[i], dayStr, 10); + strncat( + this->valueBuffer, dayStr, + this->getLength() - strlen(this->valueBuffer) - 1); + } + this->valueBuffer[this->getLength()] = '\0'; +} + +String DayParameter::renderHtml( + bool dataArrived, bool hasValueFromPost, String valueFromPost) +{ + String currentValue = + hasValueFromPost ? valueFromPost : String(this->valueBuffer); + + // Parse currently selected days + bool selectedDays[7] = {false}; + int startPos = 0; + while (startPos < currentValue.length()) + { + int commaPos = currentValue.indexOf(',', startPos); + if (commaPos == -1) + commaPos = currentValue.length(); + + String token = currentValue.substring(startPos, commaPos); + token.trim(); + int dayNum = token.toInt(); + if (dayNum >= 0 && dayNum <= 6) + { + selectedDays[dayNum] = true; + } + + startPos = commaPos + 1; + } + + // Build HTML + String html = "
"; + + const char* inputType = _multiSelect ? "checkbox" : "radio"; + + for (int i = 0; i < 7; i++) + { + int dayNumber = i; + String dayParamId = getDayParamId(dayNumber); + + html += ""; + } + + html += "
"; + + // Hidden field to store the combined value + html += ""; + + // JavaScript to update hidden field on change + html += ""; + + if (this->errorMessage != nullptr) + { + html += "
"; + html += this->errorMessage; + html += "
"; + } + + html += "
"; + + return html; +} + +void DayParameter::update(WebRequestWrapper* webRequestWrapper) +{ + if (!this->visible) + { + return; + } + + // The hidden field contains the comma-separated values + if (webRequestWrapper->hasArg(this->getId())) + { + String newValue = webRequestWrapper->arg(this->getId()); + strncpy(this->valueBuffer, newValue.c_str(), this->getLength()); + this->valueBuffer[this->getLength()] = '\0'; + } + else + { + // No value submitted (shouldn't happen with hidden field) + this->valueBuffer[0] = '\0'; + } +} + +void DayParameter::update(String newValue) +{ + strncpy(this->valueBuffer, newValue.c_str(), this->getLength()); + this->valueBuffer[this->getLength()] = '\0'; +} + +/////////////////////////////////////////////////////////////////////////////// + } // end namespace \ No newline at end of file diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index f24cd11..4c4cb45 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -53,7 +53,7 @@ class ConfigItem { public: bool visible = true; - const char* getId() { return this->_id; } + const char* getId() const { return this->_id; } /** * Calculate the size of bytes should be stored in the EEPROM. @@ -498,6 +498,95 @@ class TimeParameter : public TextParameter { friend class IotWebConf; }; +/////////////////////////////////////////////////////////////////////////////// + +/** + * Day parameter renders checkboxes or radio buttons for weekday selection. + * The value is stored as a comma-separated string (e.g., "1,3,5" for + * Mon,Wed,Fri). + */ +class DayParameter : public TextParameter +{ +public: + /** + * Create a day parameter for the config portal. + * + * @label - Displayable label at the config portal. + * @id - Identifier used for HTTP queries and as configuration key. + * @valueBuffer - Configuration value will be loaded to this buffer. + * @length - The buffer length (recommended: 32). + * @multiSelect - If true, renders checkboxes (multi-select). If false, + * renders radio buttons (single-select). + * @defaultValue - Default value (e.g., "1,2,3,4,5" for Mon-Fri). + */ + DayParameter( + const char* label, const char* id, char* valueBuffer, int length, + bool multiSelect = true, const char* defaultValue = nullptr) + : iotwebconf::TextParameter( + label, id, valueBuffer, length, defaultValue, nullptr, nullptr), + _multiSelect(multiSelect) + { + } + + /** + * Set custom day names (short names used in rendering) - applies to ALL + * instances. + * @dayNamesShort - Array of 7 short strings (Mon to Sun). + * Example: const char* germanDaysShort[7] = {"Mo", "Di", "Mi", "Do", "Fr", + * "Sa", "So"}; DayParameter::setDayNamesShort(germanDaysShort); + */ + static void setDayNamesShort(const char* dayNamesShort[7]) + { + for (int i = 0; i < 7; i++) + { + _dayNamesShort[i] = dayNamesShort[i]; + } + } + + /** + * Check if a specific day is selected. + * @dayOfWeek - Day of week (0=Sunday, 1=Monday, ..., 6=Saturday) + * @return true if the day is selected + */ + bool isDaySelected(int dayOfWeek) const; + + /** + * Get all selected days as an array. + * @selectedDays - Output array (must be at least size 7) + * @return Number of selected days (day values: 0=Sunday, 1=Monday, ..., + * 6=Saturday) + */ + int getSelectedDays(int* selectedDays) const; + + /** + * Set which days are selected. + * @selectedDays - Array of day numbers (0=Sunday, 1=Monday, ..., 6=Saturday) + * @count - Number of days in array + */ + void setSelectedDays(const int* selectedDays, int count); + +protected: + virtual String renderHtml( + bool dataArrived, bool hasValueFromPost, String valueFromPost) override; + virtual void update(WebRequestWrapper* webRequestWrapper) override; + virtual void update(String newValue) override; + +private: + friend class IotWebConf; + bool _multiSelect; + + // Helper to get child parameter ID for a specific day + String getDayParamId(int dayNumber) const + { + return String(this->getId()) + "_" + String(dayNumber); + } + + // Day names (static - shared by all instances) + static const char* _dayNamesShort[7]; +}; + +/////////////////////////////////////////////////////////////////////////////// + /** * This class is here just to make some nice indents on debug output * for group tree. From d4a11aaceb9e37d0e1de6f1991fa00143fb4d249 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 23 Jul 2026 08:37:27 +0200 Subject: [PATCH 28/30] feat: add IotWebConfTab class with tab support for configuration page --- README.md | 58 ++++++ library.properties | 2 +- src/IotWebConfTab.h | 460 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 src/IotWebConfTab.h diff --git a/README.md b/README.md index 75bed10..815cc18 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ # IotWebConf [![Build Status](https://github.com/prampec/IotWebConf/workflows/PlatformIO%20CI/badge.svg?branch=master)](https://github.com/prampec/IotWebConf/actions/workflows/test.platformio.yml) +## Table of contents +- [Upgrading to v3.0.0](#upgrading-to-v300) +- [Summary](#summary) +- [Highlights](#highlights) +- [How it works](#how-it-works) +- [Use cases](#use-cases) +- [User notes](#user-notes) +- [IotWebConf vs. WiFiManager](#iotwebconf-vs-wifimanager) +- [Security aspects](#security-aspects) +- [Compatibility](#compatibility) +- [ESPAsyncWebServer support](#espasyncwebserver-support) +- [Tab support](#tab-support) +- [Input parameters](#input-parameters) +- [Customizing and extending functionality](#customizing-and-extending-functionality) +- [TODO / Feature requests](#todo--feature-requests) +- [Known issues](#known-issues) +- [Credits](#credits) + ## Upgrading to v3.0.0 Lately version 3.0.0 is released. This release is not backward compatible with older versions, and some modification have to be done on existing codes. @@ -89,6 +107,46 @@ to ESP32. There are two major problems. - ESP8266 uses specific naming for it's classes (e.g. ESP8266WebServer). However, ESP32 uses a more generic naming (e.g. WebServer). The idea here is to use the generic naming hoping that ESP8266 will adopt these "standards" sooner or later. - ESP32 does not provide an HTTPUpdateServer implementation. So in this project we have implemented one. Whenever ESP32 provides an official HTTPUpdateServer, this local implementation will be removed. +## ESPAsyncWebServer support +This library targets the synchronous web server implementations used by ESP8266/ESP32 Arduino cores. + +If your project is based on **ESPAsyncWebServer**, please use the dedicated async extension: + - [minou65/IotWebConfAsync](https://github.com/minou65/IotWebConfAsync) + +That project provides IotWebConf functionality adapted to async server handling and is the recommended way to use IotWebConf concepts with ESPAsyncWebServer. + +## Tab support +IotWebConf also includes tab-based configuration page support via `IotWebConfTab`. +With tabs you can split large configuration forms into smaller, easier-to-navigate sections. + +### Basic usage +1. Include `IotWebConfTab.h` instead of `IotWebConf.h`. +1. Create your `IotWebConfTab` instance. +1. Assign each `ParameterGroup` to a tab name. + +```cpp +#include + +DNSServer dnsServer; +WebServer server(80); + +IotWebConfTab iotWebConf("MyThing", &dnsServer, &server, "initial123"); + +ParameterGroup networkGroup("network", "Network"); +ParameterGroup appGroup("app", "Application"); + +// Each group is rendered on the tab specified by name. +iotWebConf.addParameterGroup(&networkGroup, "Network"); +iotWebConf.addParameterGroup(&appGroup, "Application"); +``` + +### Optional tab configuration +- `setSystemTabName("System")` to customize the system tab title. +- `setSystemTabPosition(...)` to place the system tab first, last, or at a specific index. +- `setContainerWidth(minWidth, maxWidth)` to control layout width. + +If you call `addParameterGroup(group)` without a tab name, the group is added to a default tab named `Configuration`. + ## Input parameters IotWebConf provides various parameter types for creating configuration forms. Each parameter type is designed for specific use cases and renders appropriate HTML input fields. diff --git a/library.properties b/library.properties index 3c93e2a..d245003 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.4.0 +version=3.5.0 author=Balazs Kelemen maintainer=Balazs Kelemen , Andreas Zogg sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration. diff --git a/src/IotWebConfTab.h b/src/IotWebConfTab.h new file mode 100644 index 0000000..2b16674 --- /dev/null +++ b/src/IotWebConfTab.h @@ -0,0 +1,460 @@ +/** + * IotWebConfTab.h -- Extension of IotWebConf that adds tab support + * to the configuration page. + * https://github.com/minou65/IotWebConf + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#ifndef IotWebConfTab_h +#define IotWebConfTab_h + +#include +#include +#include + +namespace iotwebconf +{ + +/** + * Structure to hold tab information + */ +struct TabInfo +{ + const char* tabName; + ParameterGroup* group; +}; + +/** + * Custom HTML format provider that adds tab support + */ +class TabHtmlFormatProvider : public HtmlFormatProvider +{ +public: + TabHtmlFormatProvider( + std::vector* tabs, int minWidth = 500, int maxWidth = 600) + : _tabs(tabs), _minWidth(minWidth), _maxWidth(maxWidth) + { + } + + /** + * Set the container width constraints + */ + void setContainerWidth(int minWidth, int maxWidth) + { + _minWidth = minWidth; + _maxWidth = maxWidth; + } + + String getStyleInner() override + { + String style = HtmlFormatProvider::getStyleInner(); + + // Main container - configurable width for consistency across tabs + style += "body > div{min-width:" + String(_minWidth) + + "px;max-width:" + String(_maxWidth) + + "px;width:100%;box-sizing:border-box;}\n"; + + // Tab container styling + style += + ".tab{overflow:hidden;border-bottom:2px solid " + "#16A1E7;background-color:#f1f1f1;margin-bottom:10px;display:flex;}\n"; + // Tab button styling + style += ".tab button{background-color:#f1f1f1 !important;flex:1 1 " + "0;min-width:0;border:1px solid #ccc " + "!important;outline:none;cursor:pointer;"; + style += "padding:14px " + "16px;transition:0.3s;font-size:16px;border-top-left-radius:5px;" + "border-top-right-radius:5px;"; + style += "margin-right:2px;border-bottom:none !important;color:#333 " + "!important;line-height:normal !important;width:auto " + "!important;box-sizing:border-box;}\n"; + // Tab button hover + style += ".tab button:hover{background-color:#ddd !important;}\n"; + // Active tab button + style += + ".tab button.active{background-color:#16A1E7 !important;color:white " + "!important;border:1px solid #16A1E7 !important;border-bottom:2px " + "solid #fff !important;position:relative;z-index:1;}\n"; + // Tab content - ALL hidden by default + style += ".tabcontent{display:none;padding:12px;border:1px solid " + "#ccc;border-top:none;background-color:#fff;}\n"; + // Fieldsets consistent width + style += "fieldset{width:100%;box-sizing:border-box;}\n"; + + return style; + } + + String getScriptInner() override + { + String script = HtmlFormatProvider::getScriptInner(); + script += "function openTab(evt,tabName){var i,tabcontent,tablinks;"; + script += "tabcontent=document.getElementsByClassName('tabcontent');"; + script += + "for(i=0;i* _tabs; + int _minWidth; + int _maxWidth; +}; + +/** + * Extended IotWebConf class with tab support + */ +class IotWebConfTab : public IotWebConf +{ +public: + /** + * Create a new configuration handler with tab support. + */ + IotWebConfTab( + const char* thingName, DNSServer* dnsServer, WebServer* server, + const char* initialApPassword, const char* configVersion = "init") + : IotWebConf( + thingName, dnsServer, server, initialApPassword, configVersion), + _systemTabPosition(0) + { + _tabHtmlFormatProvider = new TabHtmlFormatProvider(&_tabs); + setHtmlFormatProvider(_tabHtmlFormatProvider); + + // System-Parameter immer zum ersten Tab hinzufügen + _systemTabName = "System"; + } + + IotWebConfTab( + const char* thingName, DNSServer* dnsServer, WebServerWrapper* server, + const char* initialApPassword, const char* configVersion = "init") + : IotWebConf( + thingName, dnsServer, server, initialApPassword, configVersion), + _systemTabPosition(0) + { + _tabHtmlFormatProvider = new TabHtmlFormatProvider(&_tabs); + setHtmlFormatProvider(_tabHtmlFormatProvider); + + _systemTabName = "System"; + } + + ~IotWebConfTab() { delete _tabHtmlFormatProvider; } + + /** + * Set the tab name for system parameters (ThingName, WiFi, AP Password). + * Must be called before init()! + */ + void setSystemTabName(const char* tabName) { _systemTabName = tabName; } + + /** + * Set the position of the system tab + * @param position Position index: + * 0 = first position (default) + * 1..n = specific position (1-based) + * -1 = last position + */ + void setSystemTabPosition(int position) { _systemTabPosition = position; } + + /** + * Set the container width constraints + */ + void setContainerWidth(int minWidth, int maxWidth) + { + if (_tabHtmlFormatProvider != nullptr) + { + _tabHtmlFormatProvider->setContainerWidth(minWidth, maxWidth); + } + } + + /** + * Add a parameter group with tab assignment. + */ + void addParameterGroup(ParameterGroup* group, const char* tabName) + { + IotWebConf::addParameterGroup(group); + + TabInfo tabInfo; + tabInfo.tabName = tabName; + tabInfo.group = group; + _tabs.push_back(tabInfo); + + // Don't add system tab name to unique tabs list + if (strcmp(tabName, _systemTabName) == 0) + { + return; + } + + if (_uniqueTabNames.find(tabName) == _uniqueTabNames.end()) + { + _uniqueTabNames[tabName] = _uniqueTabsList.size(); + _uniqueTabsList.push_back(tabName); + } + } + + /** + * Override for compatibility - adds to default "Configuration" tab + */ + void addParameterGroup(ParameterGroup* group) + { + addParameterGroup(group, "Configuration"); + } + + /** + * Config URL web request handler with tab support. + */ + void handleConfig(WebRequestWrapper* webRequestWrapper) + { + if (webRequestWrapper->hasArg("iotSave")) + { + IotWebConf::handleConfig(webRequestWrapper); + return; + } + + if (this->getState() == OnLine) + { + if (!webRequestWrapper->authenticate( + IOTWEBCONF_ADMIN_USER_NAME, getApPassword())) + { + webRequestWrapper->requestAuthentication(); + return; + } + } + + HtmlFormatProvider* hfp = getHtmlFormatProvider(); + String page = hfp->getHead(); + page.replace("{v}", String("Config ") + this->getThingName()); + page += hfp->getStyle(); + page += hfp->getScript(); + page += hfp->getHeadExtension(); + page += hfp->getHeadEnd(); + + // Generate tab buttons with system tab positioning + page += generateTabButtons(); + + page += hfp->getFormStart(); + + // Render tabs content with system tab at correct position + renderTabsContent(page); + + page += hfp->getFormEnd(); + page += getUpdateLinkHtml(); + page += getConfigVersionHtml(); + page += hfp->getEnd(); + + webRequestWrapper->sendHeader( + "Cache-Control", "no-cache, no-store, must-revalidate"); + webRequestWrapper->sendHeader("Pragma", "no-cache"); + webRequestWrapper->sendHeader("Expires", "-1"); + webRequestWrapper->send(200, "text/html; charset=UTF-8", page); + } + + void handleConfig() + { + StandardWebRequestWrapper webRequestWrapper = + StandardWebRequestWrapper(this->_standardWebServerWrapper._server); + handleConfig(&webRequestWrapper); + } + +private: + std::vector _tabs; + std::map _uniqueTabNames; + std::vector _uniqueTabsList; + TabHtmlFormatProvider* _tabHtmlFormatProvider = nullptr; + const char* _systemTabName; + int _systemTabPosition; + + String generateTabButtons() + { + String buttons = "
\n"; + + // Calculate system tab position + int systemPos = _systemTabPosition; + size_t totalCustomTabs = _uniqueTabsList.size(); + + // Clamp position to valid range + if (systemPos < 0) + { + systemPos = totalCustomTabs; // End + } + if (systemPos > (int)totalCustomTabs) + { + systemPos = totalCustomTabs; + } + + int customTabsAdded = 0; + bool systemTabAdded = false; + + // Generate all tabs in order + for (int pos = 0; pos <= (int)totalCustomTabs; pos++) + { + // Check if system tab should be at this position + if (pos == systemPos && !systemTabAdded) + { + buttons += "\n"; + systemTabAdded = true; + } + + // Add custom tab if available and we haven't added all yet + if (customTabsAdded < (int)totalCustomTabs) + { + buttons += "\n"; + customTabsAdded++; + } + } + + buttons += "
\n"; + return buttons; + } + + void renderTabsContent(String& page) + { + // Calculate system tab position + int systemPos = _systemTabPosition; + size_t totalCustomTabs = _uniqueTabsList.size(); + + // Clamp position to valid range + if (systemPos < 0) + { + systemPos = totalCustomTabs; + } + if (systemPos > (int)totalCustomTabs) + { + systemPos = totalCustomTabs; + } + + int customTabsAdded = 0; + bool systemTabAdded = false; + + // Render all tabs in order + for (int pos = 0; pos <= (int)totalCustomTabs; pos++) + { + // Check if system tab should be at this position + if (pos == systemPos && !systemTabAdded) + { + // System tab + page += "
renderHtml(false, &appender); + } + + friend TabHtmlFormatProvider; +}; + +} // namespace iotwebconf + +using iotwebconf::IotWebConfTab; + +#endif \ No newline at end of file From 021b08cda324c073e7386b7d472e87dd1b988b09 Mon Sep 17 00:00:00 2001 From: Andreas Zogg Date: Sun, 2 Aug 2026 16:09:41 +0200 Subject: [PATCH 29/30] Fix: Remove invalid size attribute from NumberParameter --- src/IotWebConfParameter.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index 4c4cb45..9fd1dea 100644 --- a/src/IotWebConfParameter.h +++ b/src/IotWebConfParameter.h @@ -345,6 +345,13 @@ class NumberParameter : public TextParameter virtual String renderHtml( bool dataArrived, bool hasValueFromPost, String valueFromPost) override; + virtual String getHtmlTemplate() override { + return F("
" + "
{e}
\n"); + } + private: friend class IotWebConf; }; From 4ea0086478fc84b99ab344fee73e0eb0aab94bc0 Mon Sep 17 00:00:00 2001 From: Andreas Zogg Date: Sun, 2 Aug 2026 16:09:57 +0200 Subject: [PATCH 30/30] Fix: Remove invalid sizeattribute from NumberParameter --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index d245003..dad4297 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=IotWebConf -version=3.5.0 +version=3.5.1 author=Balazs Kelemen maintainer=Balazs Kelemen , Andreas Zogg sentence=ESP8266/ESP32 non-blocking WiFi/AP web configuration.