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 diff --git a/.gitignore b/.gitignore index ae095a1..9a3c7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,414 @@ +## 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 + +/IotWebConf.sln +/IotWebConf.vcxitems 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 diff --git a/library.properties b/library.properties index 2c50c84..aa42957 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,10 @@ name=IotWebConf -version=3.2.1 +version=3.3.0 author=Balazs Kelemen 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 diff --git a/src/IotWebConf.cpp b/src/IotWebConf.cpp index 4b2b9b7..1bbea74 100644 --- a/src/IotWebConf.cpp +++ b/src/IotWebConf.cpp @@ -247,6 +247,30 @@ void IotWebConf::setConfigSavedCallback(std::function func) this->_configSavedCallback = 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; +} + void IotWebConf::setFormValidator( std::function func) { @@ -260,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) @@ -290,7 +329,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(); @@ -301,9 +340,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); @@ -311,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(); @@ -355,20 +383,44 @@ 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; + } page += F("Return to home page."); } page += htmlFormatProvider->getEnd(); @@ -460,12 +512,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); diff --git a/src/IotWebConf.h b/src/IotWebConf.h index 3bdb460..27f5a62 100644 --- a/src/IotWebConf.h +++ b/src/IotWebConf.h @@ -293,6 +293,24 @@ 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 + * 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. * If the method will return false, the configuration will not be saved. @@ -505,6 +523,11 @@ class IotWebConf { return &this->_systemParameters; }; + + ParameterGroup* getCustomParameterGroup() { + return &this->_customParameterGroups; + } + Parameter* getThingNameParameter() { return &this->_thingNameParameter; @@ -530,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()! @@ -558,6 +589,8 @@ class IotWebConf return this->htmlFormatProvider; } + bool validateForm(WebRequestWrapper* webRequestWrapper); + private: const char* _initialApPassword = nullptr; const char* _configVersion; @@ -600,6 +633,10 @@ 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 = &(IotWebConf::connectAp); @@ -626,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/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"; }; ///////////////////////////////////////////////////////////////////////////////// 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 60a2b1f..74f6a28 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; @@ -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 83aa153..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) { @@ -337,7 +394,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); diff --git a/src/IotWebConfParameter.h b/src/IotWebConfParameter.h index 1f83691..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; /** @@ -432,6 +441,61 @@ class SelectParameter : public OptionsParameter friend class IotWebConf; }; +/////////////////////////////////////////////////////////////////////////////// + +/** + * Date parameter is an option parameter, that rendered as HTML Date picker. + */ +class DateParameter : public 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 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. 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; /**