diff --git a/README.md b/README.md index 49c4a0d..0b31a83 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Web page disconnects may occur randomly. Simply refresh the browser if this occu ``` 1.0 : Apr-05-2022, PixelRadio V1.0, official release to the public. 1.1.1 : Jun-30-2022, PixelRadio V1.1,1, See release for changelog. +1.1.2 : Oct-18-2022, PixelRadio V1.1,2, See release for changelog. ```     diff --git a/bin/pixelradio_bin_v1_1_1.zip b/bin/pixelradio_bin_v1_1_1.zip deleted file mode 100644 index 775e685..0000000 Binary files a/bin/pixelradio_bin_v1_1_1.zip and /dev/null differ diff --git a/bin/pixelradio_bin_v1_1_2.zip b/bin/pixelradio_bin_v1_1_2.zip new file mode 100644 index 0000000..a45ea2f Binary files /dev/null and b/bin/pixelradio_bin_v1_1_2.zip differ diff --git a/docs/Hardware/README.md b/docs/Hardware/README.md index 1b15e0c..672ed5e 100644 --- a/docs/Hardware/README.md +++ b/docs/Hardware/README.md @@ -99,12 +99,15 @@ The QN8027 RF Module must be installed directly on the PCB using the SIP Header The header strips will provide a 2-3mm gap between the main PCB and the RF module. Do **NOT** use a plug-in socket to mount the RF module. -### FRONT PANEL LED +### FRONT PANEL LED (MUSIC-ON STATUS) -The green LED (Music-On indicator) requires long leads to reach the front panel. +The green "*Music On*" LED requires long leads to reach the front panel. Before soldering, temporarily position the enclosure's front panel and bend the LED's leads as needed. Then solder in place. +>Note: The *Music On* LED will be illuminated when the external audio (music source) is enabled. +It will turn off whenever the [Test Tone](../User_Manual/AdjustTab.md) function is active. + ### ESP32 INSTALLATION The LilyGo TTGO-T8 ESP32 MCU includes a 2-pin wire pigtail cable. Solder the cable to the P1 (VBAT) pads on the PCB; diff --git a/lib/ESPUI/README.md b/lib/ESPUI/README.md index 8fb2e27..f4d5978 100644 --- a/lib/ESPUI/README.md +++ b/lib/ESPUI/README.md @@ -4,7 +4,8 @@ ESPUI is a simple library to make a web-based user interface for your projects using the **ESP8266** or the **ESP32** It uses web sockets and lets you create, -control, and update elements on your GUI through multiple devices like phones + +ol, and update elements on your GUI through multiple devices like phones and tablets. ESPUI uses simple Arduino-style syntax for creating a solid, functioning user @@ -27,6 +28,7 @@ The Library runs on any kind of **ESP8266** and **ESP32** (NodeMCU, AI Thinker, * [Slider](#slider) * [Number Input](#number-input) * [Text Input](#text-input) + * [Date, Time, Colour and Password Input](#date-time-colour-and-password-input) * [Select control](#select-control) * [Getting the Time](#getting-the-time) * [Separators](#separators) @@ -41,6 +43,7 @@ The Library runs on any kind of **ESP8266** and **ESP32** (NodeMCU, AI Thinker, * [Grouped controls](#grouped-controls) * [Wide controls](#wide-controls) * [Graph (Experimental)](#graph--experimental-) + * [Captive Portal](#captive-portal) - [Notes for Development](#notes-for-development) - [Contribute](#contribute) @@ -58,6 +61,7 @@ The Library runs on any kind of **ESP8266** and **ESP32** (NodeMCU, AI Thinker, - Transport layer rework by @iangray001 - Time control by @iangray001 - Vertical controls by @iangray001 +- Time/date/password/color input types by @pcbbc ## Roadmap @@ -133,6 +137,7 @@ more program memory to work with. - Control pad - Slider - Text Input +- Date, Time, Colour and Password Input - Numberinput - Option select - Separator @@ -142,26 +147,59 @@ more program memory to work with. ## Documentation -The heart of ESPUI is -[ESPAsyncWebserver](https://github.com/me-no-dev/ESPAsyncWebServer). ESPUI's -frontend is based on [Skeleton CSS](http://getskeleton.com/) and jQuery-like -lightweight [zepto.js](https://zeptojs.com/) for handling events. The -communication between the ESP and the client browser works using web -sockets. ESPUI does not need network access and can be used in standalone access -point mode, all resources are loaded directly from the ESPs memory. - -This section will explain in detail how the Library is to be used from the -Arduino code side. In the arduino `setup()` routine the interface can be customised by adding UI Elements. -This is done by calling the corresponding library methods on the Library object -`ESPUI`. Eg: `ESPUI.button("button", &myCallback);` creates a button in the -interface that calls the `myCallback(Control *sender, int value)` function when changed. All buttons and -items call their callback whenever there is a state change from them. This means -the button will call the callback when it is pressed and also again when it is -released. To separate different events an integer number with the event name is -passed to the callback function that can be handled in a `switch(){}case{}` -statement. - - +The heart of ESPUI is [ESPAsyncWebserver](https://github.com/me-no-dev/ESPAsyncWebServer). ESPUI's frontend is based on [Skeleton CSS](http://getskeleton.com/) and jQuery-like lightweight [zepto.js](https://zeptojs.com/) for handling events. The communication between the ESP and the client browser works using web sockets. ESPUI does not need network access and can be used in standalone access point mode, all resources are loaded directly from the ESPs memory. +

+This section will explain in detail how the Library is to be used from the Arduino code side. In the arduino `setup()` routine the interface can be customised by adding UI Elements. This is done by calling the corresponding library methods on the Library object `ESPUI`. Eg: `ESPUI.button("button", &myCallback);` creates a button in the interface that calls the `myCallback(Control *sender, int eventname)` function when changed. All buttons and items call their callback whenever there is a state change from them. This means the button will call the callback when it is pressed and also again when it is released. To separate different events, an integer number with the event name is passed to the callback function that can be handled in a `switch(){}case{}` statement. +

+Alternativly you may use the extended callback funtion which provides three parameters to the callback function `myCallback(Control *sender, int eventname, void * UserParameter)`. The `UserParameter` is provided as part of the `ESPUI.addControl` method set and allows the user to define contextual information that is to be presented to the callback function in an unmodified form. +

+The below example creates a button and defines a lambda function to implicitly create an `ExtendedCallback` which then invokes a more specialized button callback handler. The example uses the `UserParameter` to hold the `this` pointer to an object instance, providing a mechanism for sending the event to a specific object without the need for a switch / map / lookup translation of the Sender Id to an object reference. +``` +void YourClassName::setup() +{ + ButtonElementId = ESPUI.addControl( + ControlType::Button, + ButtonLabel.c_str(), + " Button Face Text ", + ControlColor::None, + ParentElementId, + [](Control *sender, int eventname, void* param) + { + if(param) + { + reinterpret_cast(param)->myButtonCallback(sender, eventname); + } + }, + this); // <-Third parameter for the extended callback + + // or + ButtonElementId = ESPUI.button( + " Button Face Text ", + [](Control *sender, int eventname, void* param) + { + if(param) + { + reinterpret_cast(param)->myButtonCallback(sender, eventname); + } + }, + this); // <-Third parameter for the extended callback +} +``` +``` +void YourClassName::myButtonCallback(Control* sender, int eventname) +{ + if (eventname == B_DOWN) + { + // Handle the button down event + } + else if (eventname == B_UP) + { + // Handle the button up event + } +} +``` +
+
#### Button ![Buttons](docs/ui_button.png) @@ -289,6 +327,36 @@ Events: - `T_VALUE` - Fired when a text value changes. +#### Date, Time, Colour and Password Input + +![text](docs/ui_inputtypes.png) + +As an extension to the text input control, you can also specify the type attribute to be used for the HTML input element. +This allows you to easily create input controls for Date, Time, Colour and Passwords, or indeed any other +[HTML Input Types](https://www.w3schools.com/html/html_form_input_types.asp) supported by your browser. + +``` +text_date = ESPUI.text("Date", callback, ControlColor::Dark, "2022-05-24"); +ESPUI.setInputType(text_date, "date"); + +text_time = ESPUI.text("Time", callback, ControlColor::Dark, "13:00"); +ESPUI.setInputType(text_time, "time"); + +text_colour = ESPUI.text("Colour", callback, ControlColor::Dark, "#FF0000"); +ESPUI.setInputType(text_colour, "color"); + +text_password = ESPUI.text("Password", callback, ControlColor::Dark, "tiddles123"); +ESPUI.setInputType(text_password, "password"); +``` + +*Important!* This function should be called _before_ `ESPUI.begin` or results will be unreliable. + +Note that not all browsers support all input types, and that the control displayed to edit the input is browser dependent. + +However even with a type set, user input should still be validated +because it is easy to bypass client-side checks. Never trust user input. + + #### Select control ![option1](docs/ui_select1.png) @@ -576,6 +644,17 @@ Graph points are saved in the browser in **localstorage** to be persistant, clea _There are many issues with the graph component currently and work is ongoing. Consider helping us out with development!_ +### Captive Portal + +ESPUI will redirect all unknown URLs it is asked for to the 'root' of the local HTTP server instead of responding with an HTTP code 404. This makes it act as a simple 'captive portal'. Note you must also set up the ESP to be a DNS server that responds to all DNS requests with the IP address of the ESP. This only effective when the ESP is acting as a WiFi hotspot in AP mode and assigning itself as the DNS server to connected clients. + +All the example sketches include the DNS related code and will work as captive portals when used as a hotspot. In the event you wish to disable this feature you can do so by removing the DNS server code and adding the code below. + +``` +ESPUI.captivePortal = false; +``` + + # Notes for Development If you want to work on the HTML/CSS/JS files, do make changes in the _data_ diff --git a/lib/ESPUI/data/css/style.css b/lib/ESPUI/data/css/style.css new file mode 100644 index 0000000..f5d46f7 --- /dev/null +++ b/lib/ESPUI/data/css/style.css @@ -0,0 +1,1200 @@ +.container { + position: relative; + width: 79%; + margin: 20px; + box-sizing: border-box; +} + +.column, +.columns { + width: 100%; + float: left; +} + +.card { + min-height: 100px; + border-radius: 6px; + box-shadow: 0 4px 4px rgba(204, 197, 185, 0.5); + padding-left: 20px; + padding-right: 20px; + margin-bottom: 40px; + min-width: 500px; + color: #fff; +} + + +@media (min-width: 1205px) { + .wide.card { + min-width: 1075px; + } +} + +@media (min-width: 1790px) { + .wide.card { + min-width: 1650px; + } +} + +@media (max-width: 630px) { + .card { + min-width: 98%; + } +} + +.sectionbreak.columns { + color: black; +} + +.sectionbreak.columns hr { + border: none; + height: 2px; + background-color: #666 +} + +.card-slider {} + +.turquoise { + background: #1abc9c; + border-bottom: #16a085 3px solid; +} + +.emerald { + background: #2ecc71; + border-bottom: #27ae60 3px solid; +} + +.peterriver { + background: #3498db; + border-bottom: #2980b9 3px solid; +} + +.wetasphalt { + background: #34495e; + border-bottom: #2c3e50 3px solid; +} + +.sunflower { + background: #f1c40f; + border-bottom: #e6bb0f 3px solid; +} + +.carrot { + background: #e67e22; + border-bottom: #d35400 3px solid; +} + +.alizarin { + background: #e74c3c; + border-bottom: #c0392b 3px solid; +} + +.dark { + background: #444857; + border-bottom: #444857 3px solid; +} + +.label { + box-sizing: border-box; + white-space: nowrap; + border-radius: 0.2em; + padding: 0.12em 0.4em 0.14em; + text-align: center; + color: #ffffff; + font-weight: 700; + line-height: 1.3; + margin-bottom: 5px; + display: inline-block; + white-space: nowrap; + vertical-align: baseline; + position: relative; + top: -0.15em; + background-color: #999999; + margin-bottom: 10px; +} + +.label-wrap { + width: 90%; + white-space: pre-wrap; + word-wrap: break-word; +} + +.label.color-blue { + background-color: #6f9ad1; +} + +.label.color-red { + background-color: #d37c7c; +} + +.label.color-green { + background-color: #9bc268; +} + +.label.color-orange { + background-color: #dea154; +} + +.label.color-yellow { + background-color: #e9d641; +} + +.label.color-purple { + background-color: #9f83d1; +} + +/* For devices larger than 400px */ + +@media (min-width: 400px) { + .container { + width: 84%; + } +} + +/* For devices larger than 550px */ + +@media (min-width: 630px) { + .container { + width: 98%; + } + + .column, + .columns { + margin-right: 35px; + } + + .column:first-child, + .columns:first-child { + margin-left: 0; + } + + .one.column, + .one.columns { + width: 4.66666666667%; + } + + .two.columns { + width: 13.3333333333%; + } + + .three.columns { + width: 22%; + } + + .four.columns { + width: 30.6666666667%; + } + + .five.columns { + width: 39.3333333333%; + } + + .six.columns { + width: 48%; + } + + .seven.columns { + width: 56.6666666667%; + } + + .eight.columns { + width: 65.3333333333%; + } + + .nine.columns { + width: 74%; + } + + .ten.columns { + width: 82.6666666667%; + } + + .eleven.columns { + width: 91.3333333333%; + } + + .twelve.columns { + width: 100%; + margin-left: 0; + } + + .one-third.column { + width: 30.6666666667%; + } + + .two-thirds.column { + width: 65.3333333333%; + } + + .one-half.column { + width: 48%; + } + + /* Offsets */ + .offset-by-one.column, + .offset-by-one.columns { + margin-left: 8.66666666667%; + } + + .offset-by-two.column, + .offset-by-two.columns { + margin-left: 17.3333333333%; + } + + .offset-by-three.column, + .offset-by-three.columns { + margin-left: 26%; + } + + .offset-by-four.column, + .offset-by-four.columns { + margin-left: 34.6666666667%; + } + + .offset-by-five.column, + .offset-by-five.columns { + margin-left: 43.3333333333%; + } + + .offset-by-six.column, + .offset-by-six.columns { + margin-left: 52%; + } + + .offset-by-seven.column, + .offset-by-seven.columns { + margin-left: 60.6666666667%; + } + + .offset-by-eight.column, + .offset-by-eight.columns { + margin-left: 69.3333333333%; + } + + .offset-by-nine.column, + .offset-by-nine.columns { + margin-left: 78%; + } + + .offset-by-ten.column, + .offset-by-ten.columns { + margin-left: 86.6666666667%; + } + + .offset-by-eleven.column, + .offset-by-eleven.columns { + margin-left: 95.3333333333%; + } + + .offset-by-one-third.column, + .offset-by-one-third.columns { + margin-left: 34.6666666667%; + } + + .offset-by-two-thirds.column, + .offset-by-two-thirds.columns { + margin-left: 69.3333333333%; + } + + .offset-by-one-half.column, + .offset-by-one-half.columns { + margin-left: 52%; + } +} + +/* Base Styles + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +html { + font-size: 62.5%; +} + +body { + margin: 0; + font-size: 1.5em; + line-height: 1; + font-weight: 400; + font-family: "Open Sans", sans-serif; + color: #222; + background-color: #ecf0f1; +} + +/* Typography + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 300; +} + +h1 { + font-size: 4rem; + line-height: 1.2; + letter-spacing: -0.1rem; +} + +h2 { + font-size: 3.6rem; + line-height: 1.25; + letter-spacing: -0.1rem; +} + +h3 { + font-size: 3rem; + line-height: 1.3; + letter-spacing: -0.1rem; +} + +h4 { + font-size: 2.4rem; + line-height: 1.35; + letter-spacing: -0.08rem; +} + +h5 { + font-size: 1.8rem; + line-height: 1.5; + letter-spacing: -0.05rem; +} + +h6 { + font-size: 1.5rem; + line-height: 1.6; + letter-spacing: 0; +} + +/* Larger than phablet */ + +@media (min-width: 630px) { + h1 { + font-size: 5rem; + } + + h2 { + font-size: 4.2rem; + } + + h3 { + font-size: 3.6rem; + } + + h4 { + font-size: 3rem; + } + + h5 { + font-size: 2rem; + } + + h6 { + font-size: 1.5rem; + } +} + +p { + margin-top: 0; +} + +/* Links + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +a { + color: #1eaedb; +} + +a:hover { + color: #0fa0ce; +} + +/* Buttons + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +button { + display: inline-block; + padding: 10px; + border-radius: 3px; + color: #fff; + background-color: #999999; +} + +button:enabled:active { + background-color: #666666; + transform: translateX(4px) translateY(4px); +} + +/* Main Head Part + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +#mainHeader { + display: inline-block; +} + +#conStatus { + position: inherit; + font-size: 0.75em; +} + +/* Spacing + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +button, +.button { + margin-bottom: 1rem; + margin-left: 0.3rem; + margin-right: 0.3rem; +} + +/* Utilities + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +.u-full-width { + width: 100%; + box-sizing: border-box; +} + +.u-max-full-width { + max-width: 100%; + box-sizing: border-box; +} + +.u-pull-right { + float: right; +} + +.u-pull-left { + float: left; +} + +.tcenter { + text-align: center; +} + +/* Misc + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +hr { + margin-top: 0.5rem; + margin-bottom: 1.2rem; + border-width: 0; + border-top: 1px solid #e1e1e1; +} + +/* Clearing + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +/* Self Clearing Goodness */ + +.container:after, +.row:after, +.u-cf { + content: ""; + display: table; + clear: both; +} + +/* ButtonPad + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + +.control { + background-color: #ddd; + background-image: linear-gradient(hsla(0, 0%, 0%, 0.1), + hsla(0, 0%, 100%, 0.1)); + border-radius: 50%; + box-shadow: inset 0 1px 1px 1px hsla(0, 0%, 100%, 0.5), + 0 0 1px 1px hsla(0, 0%, 100%, 0.75), 0 0 1px 2px hsla(0, 0%, 100%, 0.25), + 0 0 1px 3px hsla(0, 0%, 100%, 0.25), 0 0 1px 4px hsla(0, 0%, 100%, 0.25), + 0 0 1px 6px hsla(0, 0%, 0%, 0.75); + height: 9em; + margin: 3em auto; + position: relative; + width: 9em; +} + +.control ul { + height: 100%; + padding: 0; + transform: rotate(45deg); +} + +.control li { + border-radius: 100% 0 0 0; + box-shadow: inset -1px -1px 1px hsla(0, 0%, 100%, 0.5), + 0 0 1px hsla(0, 0%, 0%, 0.75); + display: inline-block; + height: 50%; + overflow: hidden; + width: 50%; +} + +.control ul li:nth-child(2) { + transform: rotate(90deg); +} + +.control ul li:nth-child(3) { + transform: rotate(-90deg); +} + +.control ul li:nth-child(4) { + transform: rotate(180deg); +} + +.control ul a { + height: 200%; + position: relative; + transform: rotate(-45deg); + width: 200%; +} + +.control a:hover, +.control a:focus { + background-color: hsla(0, 0%, 100%, 0.25); +} + +.control a { + border-radius: 50%; + color: #333; + display: block; + font: bold 1em/3 sans-serif; + text-align: center; + text-decoration: none; + text-shadow: 0 1px 1px hsla(0, 0%, 100%, 0.4); + transition: 0.15s; +} + +.control .confirm { + background-color: #ddd; + background-image: linear-gradient(hsla(0, 0%, 0%, 0.15), + hsla(0, 0%, 100%, 0.25)); + box-shadow: inset 0 1px 1px 1px hsla(0, 0%, 100%, 0.5), + 0 0 1px 1px hsla(0, 0%, 100%, 0.25), 0 0 1px 2px hsla(0, 0%, 100%, 0.25), + 0 0 1px 3px hsla(0, 0%, 100%, 0.25), 0 0 1px 4px hsla(0, 0%, 100%, 0.25), + 0 0 1px 6px hsla(0, 0%, 0%, 0.85); + left: 50%; + line-height: 3; + margin: -1.5em; + position: absolute; + top: 50%; + width: 3em; +} + +.control .confirm:hover, +.control .confirm:focus { + background-color: #eee; +} + +.control:not(.disabled) a.confirm:active { + background-color:#777 +} +.control:not(.disabled) li:active { + background-color:#777 +} + +/* Switch +–––––––––––––––––––––––––––––––––––––––––––––––––– */ + +.switch { + display: inline-block !important; + background-color: #bebebe; + border-radius: 4px; + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + color: #fff; + cursor: pointer; + display: block; + font-size: 14px; + height: 26px; + margin-left: 0.3rem; + margin-right: 0.3rem; + position: relative; + width: 60px; + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -o-transition: background-color 0.2s ease-in-out; + -ms-transition: background-color 0.2s ease-in-out; + transition: background-color 0.2s ease-in-out; +} + +.switch.checked { + background-color: #76d21d; +} + +.switch input[type="checkbox"] { + display: none; + cursor: pointer; + height: 10px; + left: 12px; + position: absolute; + top: 8px; + width: 10px; +} + +.in { + position: absolute; + top: 8px; + left: 12px; + -webkit-transition: left 0.08s ease-in-out; + -moz-transition: left 0.08s ease-in-out; + -o-transition: left 0.08s ease-in-out; + -ms-transition: left 0.08s ease-in-out; + transition: left 0.08s ease-in-out; +} + +.switch.checked div { + left: 38px; +} + +.switch .in:before { + background: #fff; + background: -moz-linear-gradient(top, #fff 0%, #f0f0f0 100%); + background: -webkit-gradient(linear, + left top, + left bottom, + color-stop(0%, #fff), + color-stop(100%, #f0f0f0)); + background: -webkit-linear-gradient(top, #fff 0%, #f0f0f0 100%); + background: -o-linear-gradient(top, #fff 0%, #f0f0f0 100%); + background: -ms-linear-gradient(top, #fff 0%, #f0f0f0 100%); + background: linear-gradient(to bottom, #fff 0%, #f0f0f0 100%); + border: 1px solid #fff; + border-radius: 2px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); + content: ""; + height: 18px; + position: absolute; + top: -5px; + left: -9px; + width: 26px; +} + +.switch .in:after { + background: #f0f0f0; + background: -moz-linear-gradient(top, #f0f0f0 0%, #fff 100%); + background: -webkit-gradient(linear, + left top, + left bottom, + color-stop(0%, #f0f0f0), + color-stop(100%, #fff)); + background: -webkit-linear-gradient(top, #f0f0f0 0%, #fff 100%); + background: -o-linear-gradient(top, #f0f0f0 0%, #fff 100%); + background: -ms-linear-gradient(top, #f0f0f0 0%, #fff 100%); + background: linear-gradient(to bottom, #f0f0f0 0%, #fff 100%); + border-radius: 10px; + content: ""; + height: 12px; + margin: -1px 0 0 -1px; + position: absolute; + width: 12px; +} + +/* ---------------------------------------------------------------------- + Material Design Range Slider - by Ravikumar Chauhan + ------------------------------------------------------------------------- */ +.rkmd-slider { + display: block; + position: relative; + font-size: 16px; + font-family: "Roboto", sans-serif; +} + +.rkmd-slider input[type="range"] { + overflow: hidden; + position: absolute; + width: 1px; + height: 1px; + opacity: 0; +} + +.rkmd-slider input[type="range"]+.slider { + display: block; + position: relative; + width: 100%; + height: 27px; + border-radius: 13px; + background-color: #bebebe; +} + +@media (pointer: fine) { + .rkmd-slider input[type="range"]+.slider { + height: 4px; + border-radius: 0px; + } +} + +.rkmd-slider input[type="range"]+.slider .slider-fill { + display: block; + position: absolute; + width: 0%; + height: 100%; + user-select: none; + z-index: 1; +} + +.rkmd-slider input[type="range"]+.slider .slider-handle { + cursor: pointer; + position: absolute; + top: 12px; + left: 0%; + width: 15px; + height: 15px; + margin-left: -8px; + border-radius: 50%; + transition: all 0.2s ease; + user-select: none; + z-index: 2; +} + +@media (pointer: fine) { + .rkmd-slider input[type="range"]+.slider .slider-handle { + top: -5.5px; + } +} + +.rkmd-slider input[type="range"]:disabled+.slider { + background-color: #b0b0b0 !important; +} + +.rkmd-slider input[type="range"]:disabled+.slider .slider-fill, +.rkmd-slider input[type="range"]:disabled+.slider .slider-handle { + cursor: default !important; + background-color: #b0b0b0 !important; +} + +.rkmd-slider input[type="range"]:disabled+.slider .slider-fill .slider-label, +.rkmd-slider input[type="range"]:disabled+.slider .slider-handle .slider-label { + display: none; + background-color: #b0b0b0 !important; +} + +.rkmd-slider input[type="range"]:disabled+.slider .slider-fill.is-active, +.rkmd-slider input[type="range"]:disabled+.slider .slider-handle.is-active { + top: -5.5px; + width: 15px; + height: 15px; + margin-left: -8px; +} + +.rkmd-slider input[type="range"]:disabled+.slider .slider-fill.is-active .slider-label, +.rkmd-slider input[type="range"]:disabled+.slider .slider-handle.is-active .slider-label { + display: none; + border-radius: 50%; + transform: none; +} + +.rkmd-slider input[type="range"]:disabled+.slider .slider-handle:active { + box-shadow: none !important; + transform: scale(1) !important; +} + +/* ---------------------------------------------------------------------- + Discrete Range Slider - by Ravikumar Chauhan + ------------------------------------------------------------------------- */ +.rkmd-slider.slider-discrete .slider .slider-handle { + position: relative; + z-index: 1; +} + +.rkmd-slider.slider-discrete .slider .slider-handle .slider-label { + position: absolute; + top: -17.5px; + left: 4px; + width: 30px; + height: 30px; + -webkit-transform-origin: 50% 100%; + transform-origin: 50% 100%; + border-radius: 50%; + -webkit-transform: scale(1) rotate(-45deg); + transform: scale(1) rotate(-45deg); + -webkit-transition: all 0.2s ease; + transition: all 0.2s ease; +} + +@media (pointer: fine) { + .rkmd-slider.slider-discrete .slider .slider-handle .slider-label { + left: -2px; + -webkit-transform: scale(0.5) rotate(-45deg); + transform: scale(0.5) rotate(-45deg); + } +} + +.rkmd-slider.slider-discrete .slider .slider-handle .slider-label span { + position: absolute; + top: 7px; + left: 0px; + width: 100%; + color: #fff; + font-size: 16px; + text-align: center; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + opacity: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +@media (pointer: fine) { + .rkmd-slider.slider-discrete .slider .slider-handle .slider-label span { + font-size: 12px; + } +} + +.rkmd-slider.slider-discrete .slider .slider-handle.is-active { + top: 0px; + margin-left: -2px; + width: 4px; + height: 4px; +} + +.rkmd-slider.slider-discrete .slider .slider-handle.is-active .slider-label { + top: -15px; + left: -2px; + border-radius: 15px 15px 15px 0; + -webkit-transform: rotate(-45deg) translate(23px, -25px); + transform: rotate(-45deg) translate(23px, -25px); +} + +.rkmd-slider.slider-discrete .slider .slider-handle.is-active .slider-label span { + opacity: 1; +} + +.rkmd-slider.slider-discrete.slider-turquoise .slider-label { + background-color: #16a085; +} + +.rkmd-slider.slider-discrete.slider-emerald .slider-label { + background-color: #27ae60; +} + +.peterriver { + background: #3498db; + border-bottom: #2980b9 3px solid; +} + +.rkmd-slider.slider-discrete.slider-peterriver .slider-label { + background-color: #2980b9; +} + +.wetasphalt { + background: #34495e; + border-bottom: #2c3e50 3px solid; +} + +.rkmd-slider.slider-discrete.slider-wetasphalt .slider-label { + background-color: #2c3e50; +} + +.sunflower { + background: #f1c40f; + border-bottom: #e6bb0f 3px solid; +} + +.rkmd-slider.slider-discrete.slider-sunflower .slider-label { + background-color: #e6bb0f; +} + +.carrot { + background: #e67e22; + border-bottom: #d35400 3px solid; +} + +.rkmd-slider.slider-discrete.slider-carrot .slider-label { + background-color: #d35400; +} + +.alizarin { + background: #e74c3c; + border-bottom: #c0392b 3px solid; +} + +.rkmd-slider.slider-discrete.slider-alizarin .slider-label { + background-color: #c0392b; +} + +/* + .rkmd-slider.slider-light input[type="range"] + .slider { + background-color: #5c5c5c; + } + .rkmd-slider.slider-light input[type="range"]:disabled + .slider { + background-color: #5c5c5c !important; + } + .rkmd-slider.slider-light input[type="range"]:disabled + .slider .slider-fill, + .rkmd-slider.slider-light input[type="range"]:disabled + .slider .slider-handle { + background-color: #5c5c5c !important; + } + +*/ + +/* -------------------------------------------------------------- + * Text and number inputs + *--------------------------------------------------------------- */ + +input { + margin: 0 auto 1.2rem auto; + padding: 2px 5px; + width: 100%; + box-sizing: border-box; + border: none; + border-radius: 4px; + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + background: rgba(255, 255, 255, 0.8); +} + +select { + margin: 0 auto 1.2rem auto; + padding: 2px 5px; + width: 100%; + box-sizing: border-box; + border: none; + border-radius: 4px; + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + background: rgba(255, 255, 255, 0.8); +} + +input[id^="num"] { + max-width: 6em; + width: auto; + text-align: right; + font-weight: bold; + font-size: 115%; +} + +body div>ul.navigation { + margin: 0; + margin-bottom: 30px; + padding: 0; + border-bottom: 3px solid #666; + overflow: hidden; +} + +ul.navigation li { + list-style: none; + float: left; + margin-right: 4px; +} + +ul.navigation li.controls { + float: right; +} + +ul.navigation li a { + font-weight: bold; + display: inline-block; + padding: 6px 12px; + color: #888; + outline: 0; + text-decoration: none; + background: #f3f3f3; + background: -webkit-gradient(linear, 0 0, 0 bottom, from(#eee), to(#e4e4e4)); + background: -moz-linear-gradient(#eee, #e4e4e4); + background: linear-gradient(#eee, #e4e4e4); + -pie-background: linear-gradient(#eee, #e4e4e4); +} + +ul.navigation li.active a { + pointer-events: none; + color: white; + background: #666; + background: -webkit-gradient(linear, 0 0, 0 bottom, from(#888), to(#666)); + background: -moz-linear-gradient(#888, #666); + background: linear-gradient(#888, #666); + -pie-background: linear-gradient(#888, #666); +} + +div.tabscontent>div { + padding: 0 15px; +} + +#tabsnav:empty { + display: none; +} + +.range-slider { + margin: 0 0 0 0; +} + +.range-slider { + width: 100%; +} + +.range-slider__range { + -webkit-appearance: none; + width: calc(100% - (45px)); + height: 10px; + border-radius: 5px; + outline: 0; + padding: 0; + margin: 0; +} + +/* +.range-slider__range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + cursor: pointer; + transition: background 0.15s ease-in-out; +} +.range-slider__range::-webkit-slider-thumb:hover { + background: #1abc9c; +} +.range-slider__range:active::-webkit-slider-thumb { + background: #1abc9c; +} +.range-slider__range::-moz-range-thumb { + width: 20px; + height: 20px; + border: 0; + border-radius: 50%; + cursor: pointer; + transition: background 0.15s ease-in-out; +} +.range-slider__range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 3px #fff, 0 0 0 6px #1abc9c; +} +*/ +.range-slider__value { + display: inline-block; + position: relative; + width: 30px; + color: #fff; + line-height: 20px; + text-align: center; + border-radius: 3px; + padding: 5px 5px; + margin-left: 2px; +} + +.range-slider__value:after { + position: absolute; + top: 8px; + left: -7px; + width: 0; + height: 0; + /*border-top:1px solid transparent; + border-right:1px solid #2c3e50; + border-bottom:1px solid transparent;*/ + content: ""; +} + +::-moz-range-track { + border: 0; +} + +input::-moz-focus-inner, +input::-moz-focus-outer { + border: 0; +} + +/* Styles for Graph widget */ + +svg { + display: block; + width: 100%; + height: 100%; +} + +.y-axis path, +.x-axis path { + stroke: gray; + stroke-width: 1; + fill: none; +} + +.series { + stroke: steelblue; + stroke-width: 3; + fill: none; +} + +.data-points circle { + stroke: steelblue; + stroke-width: 2; + fill: white; +} + +.data-points text { + display: none; +} + +.data-points circle:hover { + fill: steelblue; + stroke-width: 6; +} + +.data-points circle:hover+text { + display: inline-block; +} + +text { + text-anchor: end; +} + + +/* Styles to implement vertical orientations */ + +.vert-switcher { + transform: rotate(270deg); + margin-top: 15px; + margin-bottom: 25px; +} + +.vert-slider { + width: 150px; + transform: rotate(270deg); + display: inline-block; + margin: 50px -42px 70px -42px; +} + +.vert-slider span { + transform: rotate(90deg); +} + + +/* Styles to implement disabled controls */ + +button:disabled { + color: #333; + background-color: #999; +} + +select:disabled { + color: #333; + background-color: #999; +} + +input:disabled { + color: #333; + background-color: #999; +} + +.range-slider__range:disabled { + background-color: #999; +} + +.range-slider__range:disabled::-webkit-slider-thumb { + background-color: #aaa; +} + +.range-slider__range:disabled::-moz-range-thumb { + background-color: #aaa; +} + +.switch.disabled .in::before { + background:#bbb; + border: 1px solid #ddd; +} + +.switch.disabled .in::after { + background:#bbb; +} + +.switch.checked.disabled { + background: #b1d092; +} diff --git a/lib/ESPUI/data/js/controls.js b/lib/ESPUI/data/js/controls.js index d735e40..d1dc71b 100644 --- a/lib/ESPUI/data/js/controls.js +++ b/lib/ESPUI/data/js/controls.js @@ -82,56 +82,57 @@ var hasAccel = false; var sliderContinuous = false; function colorClass(colorId) { - colorId = Number(colorId); - switch (colorId) { - case C_TURQUOISE: - return "turquoise"; + colorId = Number(colorId); + switch (colorId) { + case C_TURQUOISE: + return "turquoise"; - case C_EMERALD: - return "emerald"; + case C_EMERALD: + return "emerald"; - case C_PETERRIVER: - return "peterriver"; + case C_PETERRIVER: + return "peterriver"; - case C_WETASPHALT: - return "wetasphalt"; + case C_WETASPHALT: + return "wetasphalt"; - case C_SUNFLOWER: - return "sunflower"; + case C_SUNFLOWER: + return "sunflower"; - case C_CARROT: - return "carrot"; + case C_CARROT: + return "carrot"; - case C_ALIZARIN: - return "alizarin"; + case C_ALIZARIN: + return "alizarin"; - case C_DARK: - case C_NONE: - return "dark"; - default: - return ""; - } + case C_DARK: + case C_NONE: + return "dark"; + default: + return ""; + } } var websock; var websockConnected = false; +var WebSocketTimer = null; function requestOrientationPermission() { - /* - // Currently this fails, since it needs secure context on IOS safari - if (typeof DeviceMotionEvent.requestPermission === "function") { - DeviceOrientationEvent.requestPermission() - .then(response => { - if (response == "granted") { - window.addEventListener("deviceorientation", handleOrientation); - } - }) - .catch(console.error); - } else { - // Non IOS 13 - window.addEventListener("deviceorientation", handleOrientation); - } - */ + /* + // Currently this fails, since it needs secure context on IOS safari + if (typeof DeviceMotionEvent.requestPermission === "function") { + DeviceOrientationEvent.requestPermission() + .then(response => { + if (response == "granted") { + window.addEventListener("deviceorientation", handleOrientation); + } + }) + .catch(console.error); + } else { + // Non IOS 13 + window.addEventListener("deviceorientation", handleOrientation); + } + */ } /* function handleOrientation(event) { @@ -168,722 +169,755 @@ function handleOrientation(event) { */ function saveGraphData() { - localStorage.setItem("espuigraphs", JSON.stringify(graphData)); + localStorage.setItem("espuigraphs", JSON.stringify(graphData)); } function restoreGraphData(id) { - var savedData = localStorage.getItem("espuigraphs", graphData); - if (savedData != null) { - savedData = JSON.parse(savedData); - return savedData[id]; - } - return []; + var savedData = localStorage.getItem("espuigraphs", graphData); + if (savedData != null) { + savedData = JSON.parse(savedData); + return savedData[id]; + } + return []; } function restart() { - $(document).add("*").off(); - $("#row").html(""); - websock.close(); - start(); + $(document).add("*").off(); + $("#row").html(""); + conStatusError(); + start(); } function conStatusError() { - websockConnected = false; - $("#conStatus").removeClass("color-green"); - $("#conStatus").addClass("color-red"); - $("#conStatus").html("Error / No Connection ↻"); - $("#conStatus").off(); - $("#conStatus").on({ - click: restart, - }); + if (true === websockConnected) { + websockConnected = false; + websock.close(); + $("#conStatus").removeClass("color-green"); + $("#conStatus").addClass("color-red"); + $("#conStatus").html("Error / No Connection ↻"); + $("#conStatus").off(); + $("#conStatus").on({ + click: restart, + }); + } } function handleVisibilityChange() { - if (!websockConnected && !document.hidden) { - restart(); - } + if (!websockConnected && !document.hidden) { + restart(); + } } function start() { - document.addEventListener("visibilitychange", handleVisibilityChange, false); - if ( - window.location.port != "" || - window.location.port != 80 || - window.location.port != 443 - ) { - websock = new WebSocket( - "ws://" + window.location.hostname + ":" + window.location.port + "/ws" - ); - } else { - websock = new WebSocket("ws://" + window.location.hostname + "/ws"); - } - websock.onopen = function (evt) { - console.log("websock open"); - $("#conStatus").addClass("color-green"); - $("#conStatus").text("Connected"); - websockConnected = true; - }; - - websock.onclose = function (evt) { - console.log("websock close"); - conStatusError(); - }; - - websock.onerror = function (evt) { - console.log(evt); - conStatusError(); - }; - - var handleEvent = function (evt) { - console.log(evt); - var data = JSON.parse(evt.data); - var e = document.body; - var center = ""; - - switch (data.type) { - case UI_INITIAL_GUI: - // Clear current elements - $("#row").html(""); - $("#tabsnav").html(""); - $("#tabscontent").html(""); - - if (data.sliderContinuous) { - sliderContinuous = data.sliderContinuous; - } - data.controls.forEach(element => { - var fauxEvent = { - data: JSON.stringify(element), - }; - handleEvent(fauxEvent); - }); - - //If there are more elements in the complete UI, then request them - //Note: we subtract 1 from data.controls.length because the controls always - //includes the title element - if(data.totalcontrols > (data.controls.length - 1)) { - websock.send("uiok:" + (data.controls.length - 1)); - } - break; - - case UI_EXTEND_GUI: - data.controls.forEach(element => { - var fauxEvent = { - data: JSON.stringify(element), - }; - handleEvent(fauxEvent); - }); - - //Do we need to keep requesting more UI elements? - if(data.totalcontrols > data.startindex + (data.controls.length - 1)) { - websock.send("uiok:" + (data.startindex + (data.controls.length - 1))); - } - break; - - case UI_RELOAD: - window.location.reload(); - break; - - case UI_TITEL: - document.title = data.label; - $("#mainHeader").html(data.label); - break; - - /* - Most elements have the same behaviour when added. - */ - case UI_LABEL: - case UI_NUMBER: - case UI_TEXT_INPUT: - case UI_SELECT: - case UI_GAUGE: - case UI_SEPARATOR: - if (data.visible) addToHTML(data); - break; - - /* - These elements must call additional functions after being added to the DOM - */ - case UI_BUTTON: - if (data.visible) { - addToHTML(data); - $("#btn" + data.id).on({ - touchstart: function (e) { - e.preventDefault(); - buttonclick(data.id, true); - }, - touchend: function (e) { - e.preventDefault(); - buttonclick(data.id, false); - }, - }); - } - break; - - case UI_SWITCHER: - if (data.visible) { - addToHTML(data); - switcher(data.id, data.value); - } - break; - - case UI_CPAD: - case UI_PAD: - if (data.visible) { - addToHTML(data); - $("#pf" + data.id).on({ - touchstart: function (e) { - e.preventDefault(); - padclick(UP, data.id, true); - }, - touchend: function (e) { - e.preventDefault(); - padclick(UP, data.id, false); - }, - }); - $("#pl" + data.id).on({ - touchstart: function (e) { - e.preventDefault(); - padclick(LEFT, data.id, true); - }, - touchend: function (e) { - e.preventDefault(); - padclick(LEFT, data.id, false); - }, - }); - $("#pr" + data.id).on({ - touchstart: function (e) { - e.preventDefault(); - padclick(RIGHT, data.id, true); - }, - touchend: function (e) { - e.preventDefault(); - padclick(RIGHT, data.id, false); - }, - }); - $("#pb" + data.id).on({ - touchstart: function (e) { - e.preventDefault(); - padclick(DOWN, data.id, true); - }, - touchend: function (e) { - e.preventDefault(); - padclick(DOWN, data.id, false); - }, - }); - $("#pc" + data.id).on({ - touchstart: function (e) { - e.preventDefault(); - padclick(CENTER, data.id, true); - }, - touchend: function (e) { - e.preventDefault(); - padclick(CENTER, data.id, false); - }, - }); - } - break; - - case UI_SLIDER: - //https://codepen.io/seanstopnik/pen/CeLqA - if (data.visible) { - addToHTML(data); - rangeSlider(!sliderContinuous); - } - break; - - case UI_TAB: - if (data.visible) { - $("#tabsnav").append( - "
  • " + data.value + "
  • " - ); - $("#tabscontent").append("
    "); - - tabs = $(".tabscontent").tabbedContent({ loop: true }).data("api"); - // switch to tab... - $("a") - .filter(function () { - return $(this).attr("href") === "#click-to-switch"; - }) - .on("click", function (e) { - var tab = prompt("Tab to switch to (number or id)?"); - if (!tabs.switchTab(tab)) { - alert("That tab does not exist :\\"); - } - e.preventDefault(); - }); - } - break; - - case UI_OPTION: - if (data.parentControl) { - var parent = $("#select" + data.parentControl); - parent.append( - "" - ); - } - break; - - case UI_MIN: - if (data.parentControl) { - //Is it applied to a slider? - if($('#sl' + data.parentControl).length) { - $('#sl' + data.parentControl).attr("min", data.value); - } else if($('#num' + data.parentControl).length) { - //Or a number - $('#num' + data.parentControl).attr("min", data.value); - } - } - break; - - case UI_MAX: - if (data.parentControl) { - //Is it applied to a slider? - if($('#sl' + data.parentControl).length) { - $('#sl' + data.parentControl).attr("max", data.value); - } else if($('#text' + data.parentControl).length) { - //Is it a text element - $('#text' + data.parentControl).attr("maxlength", data.value); - } else if($('#num' + data.parentControl).length) { - //Or a number - $('#num' + data.parentControl).attr("max", data.value); - } - } - break; - - case UI_STEP: - if (data.parentControl) { - var parent = $("#id" + data.parentControl + " input"); - if (parent.size()) { - parent.attr("step", data.value); - } - } - break; - - case UI_GRAPH: - if (data.visible) { - addToHTML(data); - graphData[data.id] = restoreGraphData(data.id); - renderGraphSvg(graphData[data.id], "graph" + data.id); - } - break; - case ADD_GRAPH_POINT: - var ts = Math.round(new Date().getTime() / 1000); - graphData[data.id].push({ x: ts, y: data.value }); - saveGraphData(); - renderGraphSvg(graphData[data.id], "graph" + data.id); - break; - case CLEAR_GRAPH: - graphData[data.id] = []; - saveGraphData(); - renderGraphSvg(graphData[data.id], "graph" + data.id); - break; - - case UI_ACCEL: - if (hasAccel) break; - hasAccel = true; - if (data.visible) { - addToHTML(data); - requestOrientationPermission(); - } - break; - - /* - * Update messages change the value/style of a component without adding new HTML - */ - case UPDATE_LABEL: - $("#l" + data.id).html(data.value); - if(data.hasOwnProperty('elementStyle')) { - $("#l" + data.id).attr("style", data.elementStyle); - } - break; - - case UPDATE_SWITCHER: - switcher(data.id, data.value == "0" ? 0 : 1); - if(data.hasOwnProperty('elementStyle')) { - $("#sl" + data.id).attr("style", data.elementStyle); - } - break; + document.addEventListener("visibilitychange", handleVisibilityChange, false); + if ( + window.location.port != "" || + window.location.port != 80 || + window.location.port != 443 + ) { + websock = new WebSocket( + "ws://" + window.location.hostname + ":" + window.location.port + "/ws" + ); + } else { + websock = new WebSocket("ws://" + window.location.hostname + "/ws"); + } - case UPDATE_SLIDER: - $("#sl" + data.id).attr("value", data.value) - slider_move($("#id" + data.id), data.value, "100", false); - if(data.hasOwnProperty('elementStyle')) { - $("#sl" + data.id).attr("style", data.elementStyle); + // is the timer running? + if (null === WebSocketTimer) { + // timer runs forever + WebSocketTimer = setInterval(function () { + // console.info("Periodic Timer has expired"); + // is the socket closed? + if (websock.readyState === 3) { + // console.info("Web Socket Is Closed"); + restart(); + } + }, 5000); + } // end timer was not running + + websock.onopen = function (evt) { + console.log("websock open"); + $("#conStatus").addClass("color-green"); + $("#conStatus").text("Connected"); + websockConnected = true; + }; + + websock.onclose = function (evt) { + console.log("websock close"); + conStatusError(); + }; + + websock.onerror = function (evt) { + console.log("websock Error"); + console.log(evt); + + restart(); + }; + + var handleEvent = function (evt) { + console.log(evt); + try { + var data = JSON.parse(evt.data); } - break; - - case UPDATE_NUMBER: - $("#num" + data.id).val(data.value); - if(data.hasOwnProperty('elementStyle')) { - $("#num" + data.id).attr("style", data.elementStyle); + catch (Event) { + console.error(Event); + // start the update over again + websock.send("uiok:" + 0); + return; } - break; - - case UPDATE_TEXT_INPUT: - $("#text" + data.id).val(data.value); - if(data.hasOwnProperty('elementStyle')) { - $("#text" + data.id).attr("style", data.elementStyle); + var e = document.body; + var center = ""; + + switch (data.type) { + case UI_INITIAL_GUI: + // Clear current elements + $("#row").html(""); + $("#tabsnav").html(""); + $("#tabscontent").html(""); + + if (data.sliderContinuous) { + sliderContinuous = data.sliderContinuous; + } + data.controls.forEach(element => { + var fauxEvent = { + data: JSON.stringify(element), + }; + handleEvent(fauxEvent); + }); + + //If there are more elements in the complete UI, then request them + //Note: we subtract 1 from data.controls.length because the controls always + //includes the title element + if (data.totalcontrols > (data.controls.length - 1)) { + websock.send("uiok:" + (data.controls.length - 1)); + } + break; + + case UI_EXTEND_GUI: + data.controls.forEach(element => { + var fauxEvent = { + data: JSON.stringify(element), + }; + handleEvent(fauxEvent); + }); + + //Do we need to keep requesting more UI elements? + if (data.totalcontrols > data.startindex + (data.controls.length - 1)) { + websock.send("uiok:" + (data.startindex + (data.controls.length - 1))); + } + break; + + case UI_RELOAD: + window.location.reload(); + break; + + case UI_TITEL: + document.title = data.label; + $("#mainHeader").html(data.label); + break; + + /* + Most elements have the same behaviour when added. + */ + case UI_LABEL: + case UI_NUMBER: + case UI_TEXT_INPUT: + case UI_SELECT: + case UI_GAUGE: + case UI_SEPARATOR: + if (data.visible) addToHTML(data); + break; + + /* + These elements must call additional functions after being added to the DOM + */ + case UI_BUTTON: + if (data.visible) { + addToHTML(data); + $("#btn" + data.id).on({ + touchstart: function (e) { + e.preventDefault(); + buttonclick(data.id, true); + }, + touchend: function (e) { + e.preventDefault(); + buttonclick(data.id, false); + }, + }); + } + break; + + case UI_SWITCHER: + if (data.visible) { + addToHTML(data); + switcher(data.id, data.value); + } + break; + + case UI_CPAD: + case UI_PAD: + if (data.visible) { + addToHTML(data); + $("#pf" + data.id).on({ + touchstart: function (e) { + e.preventDefault(); + padclick(UP, data.id, true); + }, + touchend: function (e) { + e.preventDefault(); + padclick(UP, data.id, false); + }, + }); + $("#pl" + data.id).on({ + touchstart: function (e) { + e.preventDefault(); + padclick(LEFT, data.id, true); + }, + touchend: function (e) { + e.preventDefault(); + padclick(LEFT, data.id, false); + }, + }); + $("#pr" + data.id).on({ + touchstart: function (e) { + e.preventDefault(); + padclick(RIGHT, data.id, true); + }, + touchend: function (e) { + e.preventDefault(); + padclick(RIGHT, data.id, false); + }, + }); + $("#pb" + data.id).on({ + touchstart: function (e) { + e.preventDefault(); + padclick(DOWN, data.id, true); + }, + touchend: function (e) { + e.preventDefault(); + padclick(DOWN, data.id, false); + }, + }); + $("#pc" + data.id).on({ + touchstart: function (e) { + e.preventDefault(); + padclick(CENTER, data.id, true); + }, + touchend: function (e) { + e.preventDefault(); + padclick(CENTER, data.id, false); + }, + }); + } + break; + + case UI_SLIDER: + //https://codepen.io/seanstopnik/pen/CeLqA + if (data.visible) { + addToHTML(data); + rangeSlider(!sliderContinuous); + } + break; + + case UI_TAB: + if (data.visible) { + $("#tabsnav").append( + "
  • " + data.value + "
  • " + ); + $("#tabscontent").append("
    "); + + tabs = $(".tabscontent").tabbedContent({ loop: true }).data("api"); + // switch to tab... + $("a") + .filter(function () { + return $(this).attr("href") === "#click-to-switch"; + }) + .on("click", function (e) { + var tab = prompt("Tab to switch to (number or id)?"); + if (!tabs.switchTab(tab)) { + alert("That tab does not exist :\\"); + } + e.preventDefault(); + }); + } + break; + + case UI_OPTION: + if (data.parentControl) { + var parent = $("#select" + data.parentControl); + parent.append( + "" + ); + } + break; + + case UI_MIN: + if (data.parentControl) { + //Is it applied to a slider? + if ($('#sl' + data.parentControl).length) { + $('#sl' + data.parentControl).attr("min", data.value); + } else if ($('#num' + data.parentControl).length) { + //Or a number + $('#num' + data.parentControl).attr("min", data.value); + } + } + break; + + case UI_MAX: + if (data.parentControl) { + //Is it applied to a slider? + if ($('#sl' + data.parentControl).length) { + $('#sl' + data.parentControl).attr("max", data.value); + } else if ($('#text' + data.parentControl).length) { + //Is it a text element + $('#text' + data.parentControl).attr("maxlength", data.value); + } else if ($('#num' + data.parentControl).length) { + //Or a number + $('#num' + data.parentControl).attr("max", data.value); + } + } + break; + + case UI_STEP: + if (data.parentControl) { + var parent = $("#id" + data.parentControl + " input"); + if (parent.size()) { + parent.attr("step", data.value); + } + } + break; + + case UI_GRAPH: + if (data.visible) { + addToHTML(data); + graphData[data.id] = restoreGraphData(data.id); + renderGraphSvg(graphData[data.id], "graph" + data.id); + } + break; + case ADD_GRAPH_POINT: + var ts = Math.round(new Date().getTime() / 1000); + graphData[data.id].push({ x: ts, y: data.value }); + saveGraphData(); + renderGraphSvg(graphData[data.id], "graph" + data.id); + break; + case CLEAR_GRAPH: + graphData[data.id] = []; + saveGraphData(); + renderGraphSvg(graphData[data.id], "graph" + data.id); + break; + + case UI_ACCEL: + if (hasAccel) break; + hasAccel = true; + if (data.visible) { + addToHTML(data); + requestOrientationPermission(); + } + break; + + /* + * Update messages change the value/style of a component without adding new HTML + */ + case UPDATE_LABEL: + $("#l" + data.id).html(data.value); + if (data.hasOwnProperty('elementStyle')) { + $("#l" + data.id).attr("style", data.elementStyle); + } + break; + + case UPDATE_SWITCHER: + switcher(data.id, data.value == "0" ? 0 : 1); + if (data.hasOwnProperty('elementStyle')) { + $("#sl" + data.id).attr("style", data.elementStyle); + } + break; + + case UPDATE_SLIDER: + $("#sl" + data.id).attr("value", data.value) + slider_move($("#sl" + data.id).parent().parent(), data.value, "100", false); + if (data.hasOwnProperty('elementStyle')) { + $("#sl" + data.id).attr("style", data.elementStyle); + } + break; + + case UPDATE_NUMBER: + $("#num" + data.id).val(data.value); + if (data.hasOwnProperty('elementStyle')) { + $("#num" + data.id).attr("style", data.elementStyle); + } + break; + + case UPDATE_TEXT_INPUT: + $("#text" + data.id).val(data.value); + if (data.hasOwnProperty('elementStyle')) { + $("#text" + data.id).attr("style", data.elementStyle); + } + if (data.hasOwnProperty('inputType')) { + $("#text" + data.id).attr("type", data.inputType); + } + break; + + case UPDATE_SELECT: + $("#select" + data.id).val(data.value); + if (data.hasOwnProperty('elementStyle')) { + $("#select" + data.id).attr("style", data.elementStyle); + } + break; + + case UPDATE_BUTTON: + $("#btn" + data.id).val(data.value); + $("#btn" + data.id).text(data.value); + if (data.hasOwnProperty('elementStyle')) { + $("#btn" + data.id).attr("style", data.elementStyle); + } + break; + + case UPDATE_PAD: + case UPDATE_CPAD: + break; + case UPDATE_GAUGE: + $("#gauge" + data.id).val(data.value); + if (data.hasOwnProperty('elementStyle')) { + $("#gauge" + data.id).attr("style", data.elementStyle); + } + break; + case UPDATE_ACCEL: + break; + + case UPDATE_TIME: + var rv = new Date().toISOString(); + websock.send("time:" + rv + ":" + data.id); + break; + + default: + console.error("Unknown type or event"); + break; } - break; - case UPDATE_SELECT: - $("#select" + data.id).val(data.value); - if(data.hasOwnProperty('elementStyle')) { - $("#select" + data.id).attr("style", data.elementStyle); + if (data.type >= UI_TITEL && data.type < UPDATE_OFFSET) { + //A UI element was just added to the DOM + processEnabled(data); } - break; - case UPDATE_BUTTON: - $("#btn" + data.id).val(data.value); - $("#btn" + data.id).text(data.value); - if(data.hasOwnProperty('elementStyle')) { - $("#btn" + data.id).attr("style", data.elementStyle); - } - break; - - case UPDATE_PAD: - case UPDATE_CPAD: - break; - case UPDATE_GAUGE: - $("#gauge" + data.id).val(data.value); - if(data.hasOwnProperty('elementStyle')) { - $("#gauge" + data.id).attr("style", data.elementStyle); + if (data.type >= UPDATE_OFFSET && data.type < UI_INITIAL_GUI) { + //An "update" message was just recieved and processed + var element = $("#id" + data.id); + + if (data.hasOwnProperty('panelStyle')) { + $("#id" + data.id).attr("style", data.panelStyle); + } + + if (data.hasOwnProperty('visible')) { + if (data['visible']) + $("#id" + data.id).show(); + else + $("#id" + data.id).hide(); + } + + if (data.type == UPDATE_SLIDER) { + element.removeClass( + "slider-turquoise slider-emerald slider-peterriver slider-wetasphalt slider-sunflower slider-carrot slider-alizarin" + ); + element.addClass("slider-" + colorClass(data.color)); + } else { + element.removeClass( + "turquoise emerald peterriver wetasphalt sunflower carrot alizarin" + ); + element.addClass(colorClass(data.color)); + } + + processEnabled(data); } - break; - case UPDATE_ACCEL: - break; - - case UPDATE_TIME: - var rv = new Date().toISOString(); - websock.send("time:" + rv + ":" + data.id); - break; - - default: - console.error("Unknown type or event"); - break; - } - - if (data.type >= UI_TITEL && data.type < UPDATE_OFFSET) { - //A UI element was just added to the DOM - processEnabled(data); - } - - if (data.type >= UPDATE_OFFSET && data.type < UI_INITIAL_GUI) { - //An "update" message was just recieved and processed - var element = $("#id" + data.id); - - if(data.hasOwnProperty('panelStyle')) { - $("#id" + data.id).attr("style", data.panelStyle); - } - if(data.hasOwnProperty('visible')) { - if(data['visible']) - $("#id" + data.id).show(); - else - $("#id" + data.id).hide(); - } - - if (data.type == UPDATE_SLIDER) { - element.removeClass( - "slider-turquoise slider-emerald slider-peterriver slider-wetasphalt slider-sunflower slider-carrot slider-alizarin" - ); - element.addClass("slider-" + colorClass(data.color)); - } else { - element.removeClass( - "turquoise emerald peterriver wetasphalt sunflower carrot alizarin" - ); - element.addClass(colorClass(data.color)); - } - - processEnabled(data); - } - - $(".range-slider__range").each(function(){ - $(this)[0].value = $(this).attr("value"); - $(this).next().html($(this).attr("value")); - }); - }; + $(".range-slider__range").each(function () { + $(this)[0].value = $(this).attr("value"); + $(this).next().html($(this).attr("value")); + }); + }; - websock.onmessage = handleEvent; + websock.onmessage = handleEvent; } function sliderchange(number) { - var val = $("#sl" + number).val(); - websock.send("slvalue:" + val + ":" + number); + var val = $("#sl" + number).val(); + websock.send("slvalue:" + val + ":" + number); - $(".range-slider__range").each(function(){ - $(this).attr("value", $(this)[0].value); - }); + $(".range-slider__range").each(function () { + $(this).attr("value", $(this)[0].value); + }); } function numberchange(number) { - var val = $("#num" + number).val(); - websock.send("nvalue:" + val + ":" + number); + var val = $("#num" + number).val(); + websock.send("nvalue:" + val + ":" + number); } function textchange(number) { - var val = $("#text" + number).val(); - websock.send("tvalue:" + val + ":" + number); + var val = $("#text" + number).val(); + websock.send("tvalue:" + val + ":" + number); } function tabclick(number) { - var val = $("#tab" + number).val(); - websock.send("tabvalue:" + val + ":" + number); + var val = $("#tab" + number).val(); + websock.send("tabvalue:" + val + ":" + number); } function selectchange(number) { - var val = $("#select" + number).val(); - websock.send("svalue:" + val + ":" + number); + var val = $("#select" + number).val(); + websock.send("svalue:" + val + ":" + number); } function buttonclick(number, isdown) { - if (isdown) websock.send("bdown:" + number); - else websock.send("bup:" + number); + if (isdown) websock.send("bdown:" + number); + else websock.send("bup:" + number); } function padclick(type, number, isdown) { -if($("#id" + number + " nav").hasClass("disabled")) { - return; - } - switch (type) { - case CENTER: - if (isdown) websock.send("pcdown:" + number); - else websock.send("pcup:" + number); - break; - case UP: - if (isdown) websock.send("pfdown:" + number); - else websock.send("pfup:" + number); - break; - case DOWN: - if (isdown) websock.send("pbdown:" + number); - else websock.send("pbup:" + number); - break; - case LEFT: - if (isdown) websock.send("pldown:" + number); - else websock.send("plup:" + number); - break; - case RIGHT: - if (isdown) websock.send("prdown:" + number); - else websock.send("prup:" + number); - break; - } + if ($("#id" + number + " nav").hasClass("disabled")) { + return; + } + switch (type) { + case CENTER: + if (isdown) websock.send("pcdown:" + number); + else websock.send("pcup:" + number); + break; + case UP: + if (isdown) websock.send("pfdown:" + number); + else websock.send("pfup:" + number); + break; + case DOWN: + if (isdown) websock.send("pbdown:" + number); + else websock.send("pbup:" + number); + break; + case LEFT: + if (isdown) websock.send("pldown:" + number); + else websock.send("plup:" + number); + break; + case RIGHT: + if (isdown) websock.send("prdown:" + number); + else websock.send("prup:" + number); + break; + } } function switcher(number, state) { - if (state == null) { - if (!$("#sl" + number).hasClass("checked")) { - websock.send("sactive:" + number); - $("#sl" + number).addClass("checked"); - } else { - websock.send("sinactive:" + number); - $("#sl" + number).removeClass("checked"); + if (state == null) { + if (!$("#sl" + number).hasClass("checked")) { + websock.send("sactive:" + number); + $("#sl" + number).addClass("checked"); + } else { + websock.send("sinactive:" + number); + $("#sl" + number).removeClass("checked"); + } + } else if (state == 1) { + $("#sl" + number).addClass("checked"); + $("#sl" + number).prop("checked", true); + } else if (state == 0) { + $("#sl" + number).removeClass("checked"); + $("#sl" + number).prop("checked", false); } - } else if (state == 1) { - $("#sl" + number).addClass("checked"); - $("#sl" + number).prop("checked", true); - } else if (state == 0) { - $("#sl" + number).removeClass("checked"); - $("#sl" + number).prop("checked", false); - } } var rangeSlider = function (isDiscrete) { - var range = $(".range-slider__range"); - var slidercb = function() { - sliderchange($(this).attr("id").replace(/^\D+/g, "")); - }; - - range.on({input: function() { - $(this).next().html(this.value)} - }); - - range.each(function() { - $(this).next().html(this.value); - if($(this).attr("callbackSet") != "true") { - if (!isDiscrete) { - $(this).on({input: slidercb}); //input fires when dragging - } else { - $(this).on({change: slidercb}); //change fires only once released - } - $(this).attr("callbackSet", "true"); - } - }); + var range = $(".range-slider__range"); + var slidercb = function () { + sliderchange($(this).attr("id").replace(/^\D+/g, "")); + }; + + range.on({ + input: function () { + $(this).next().html(this.value) + } + }); + + range.each(function () { + $(this).next().html(this.value); + if ($(this).attr("callbackSet") != "true") { + if (!isDiscrete) { + $(this).on({ input: slidercb }); //input fires when dragging + } else { + $(this).on({ change: slidercb }); //change fires only once released + } + $(this).attr("callbackSet", "true"); + } + }); }; -var addToHTML = function(data) { - panelStyle = data.hasOwnProperty('panelStyle') ? " style='" + data.panelStyle + "' " : ""; - panelwide = data.hasOwnProperty('wide') ? "wide" : ""; - - if(!data.hasOwnProperty('parentControl') || $("#tab" + data.parentControl).length > 0) { - //We add the control with its own panel - var parent = data.hasOwnProperty('parentControl') ? - $("#tab" + data.parentControl) : - $("#row"); - - var html = ""; - switch(data.type) { - case UI_LABEL: - case UI_BUTTON: - case UI_SWITCHER: - case UI_CPAD: - case UI_PAD: - case UI_SLIDER: - case UI_NUMBER: - case UI_TEXT_INPUT: - case UI_SELECT: - case UI_GRAPH: - case UI_GAUGE: - case UI_ACCEL: - html = "
    " + data.label + "

    " + - elementHTML(data) + - "
    "; - break; - case UI_SEPARATOR: - html = "
    " + - "
    " + data.label + "

    "; - break; - case UI_TIME: - //Invisible element - break; - } +var addToHTML = function (data) { + panelStyle = data.hasOwnProperty('panelStyle') ? " style='" + data.panelStyle + "' " : ""; + panelwide = data.hasOwnProperty('wide') ? "wide" : ""; + + if (!data.hasOwnProperty('parentControl') || $("#tab" + data.parentControl).length > 0) { + //We add the control with its own panel + var parent = data.hasOwnProperty('parentControl') ? + $("#tab" + data.parentControl) : + $("#row"); + + var html = ""; + switch (data.type) { + case UI_LABEL: + case UI_BUTTON: + case UI_SWITCHER: + case UI_CPAD: + case UI_PAD: + case UI_SLIDER: + case UI_NUMBER: + case UI_TEXT_INPUT: + case UI_SELECT: + case UI_GRAPH: + case UI_GAUGE: + case UI_ACCEL: + html = "
    " + data.label + "

    " + + elementHTML(data) + + "
    "; + break; + case UI_SEPARATOR: + html = "
    " + + "
    " + data.label + "

    "; + break; + case UI_TIME: + //Invisible element + break; + } - parent.append(html); + parent.append(html); - } else { - //We are adding to an existing panel so we only need the HTML for the element - var parent = $("#id" + data.parentControl); - parent.append(elementHTML(data)); - } + } else { + //We are adding to an existing panel so we only need the HTML for the element + var parent = $("#id" + data.parentControl); + parent.append(elementHTML(data)); + } } -var elementHTML = function(data) { - var id = data.id - var elementStyle = data.hasOwnProperty('elementStyle') ? " style='" + data.elementStyle + "' " : ""; - switch(data.type) { - case UI_LABEL: - return "" + data.value + ""; - case UI_BUTTON: - return ""; - case UI_SWITCHER: - return ""; - case UI_CPAD: - case UI_PAD: - return ""; - case UI_SLIDER: - return "
    " + - "" + - data.value + "
    "; - case UI_NUMBER: - return ""; - case UI_TEXT_INPUT: - return ""; - case UI_SELECT: - return ""; - case UI_ACCEL: - return "ACCEL // Not implemented fully!
    ";
    -    default:
    -      return "";
    -  }
    +var elementHTML = function (data) {
    +    var id = data.id
    +    var elementStyle = data.hasOwnProperty('elementStyle') ? " style='" + data.elementStyle + "' " : "";
    +    var inputType = data.hasOwnProperty('inputType') ? " type='" + data.inputType + "' " : "";
    +    switch (data.type) {
    +        case UI_LABEL:
    +            return "" + data.value + "";
    +        case UI_BUTTON:
    +            return "";
    +        case UI_SWITCHER:
    +            return "";
    +        case UI_CPAD:
    +        case UI_PAD:
    +            return "";
    +        case UI_SLIDER:
    +            return "
    " + + "" + + data.value + "
    "; + case UI_NUMBER: + return ""; + case UI_TEXT_INPUT: + return ""; + case UI_SELECT: + return ""; + case UI_ACCEL: + return "ACCEL // Not implemented fully!
    ";
    +        default:
    +            return "";
    +    }
     }
     
     
     
    -var processEnabled = function(data) {
    -  //Handle the enabling and disabling of controls
    -  //Most controls can be disabled through the use of $("#").prop("disabled", true) and CSS will style it accordingly
    -  //The switcher and pads also require the addition of the "disabled" class
    -  switch(data.type) {
    -    case UI_SWITCHER:
    -    case UPDATE_SWITCHER:
    -      if(data.enabled) {
    -        $("#sl" + data.id).removeClass('disabled');
    -        $("#s" + data.id).prop("disabled", false);
    -      } else {
    -        $("#sl" + data.id).addClass('disabled');
    -        $("#s" + data.id).prop("disabled", true);
    -      }
    -      break;
    -      
    -    case UI_SLIDER:
    -    case UPDATE_SLIDER:
    -      $("#sl" + data.id).prop("disabled", !data.enabled);
    -      break;
    -
    -    case UI_NUMBER:
    -    case UPDATE_NUMBER:
    -      $("#num" + data.id).prop("disabled", !data.enabled);
    -      break;
    -
    -    case UI_TEXT_INPUT:
    -    case UPDATE_TEXT_INPUT:
    -      $("#text" + data.id).prop("disabled", !data.enabled);
    -      break;
    -    
    -    case UI_SELECT:
    -    case UPDATE_SELECT:
    -      $("#select" + data.id).prop("disabled", !data.enabled);
    -      break;
    -
    -    case UI_BUTTON:
    -    case UPDATE_BUTTON:
    -      $("#btn" + data.id).prop("disabled", !data.enabled);
    -      break;
    -
    -    case UI_PAD:
    -    case UI_CPAD:
    -    case UPDATE_PAD:
    -    case UPDATE_CPAD:
    -      if(data.enabled) {
    -        $("#id" + data.id + " nav").removeClass('disabled');
    -      } else {
    -        $("#id" + data.id + " nav").addClass('disabled');
    -      }
    -      break;
    -  }
    +var processEnabled = function (data) {
    +    //Handle the enabling and disabling of controls
    +    //Most controls can be disabled through the use of $("#").prop("disabled", true) and CSS will style it accordingly
    +    //The switcher and pads also require the addition of the "disabled" class
    +    switch (data.type) {
    +        case UI_SWITCHER:
    +        case UPDATE_SWITCHER:
    +            if (data.enabled) {
    +                $("#sl" + data.id).removeClass('disabled');
    +                $("#s" + data.id).prop("disabled", false);
    +            } else {
    +                $("#sl" + data.id).addClass('disabled');
    +                $("#s" + data.id).prop("disabled", true);
    +            }
    +            break;
    +
    +        case UI_SLIDER:
    +        case UPDATE_SLIDER:
    +            $("#sl" + data.id).prop("disabled", !data.enabled);
    +            break;
    +
    +        case UI_NUMBER:
    +        case UPDATE_NUMBER:
    +            $("#num" + data.id).prop("disabled", !data.enabled);
    +            break;
    +
    +        case UI_TEXT_INPUT:
    +        case UPDATE_TEXT_INPUT:
    +            $("#text" + data.id).prop("disabled", !data.enabled);
    +            break;
    +
    +        case UI_SELECT:
    +        case UPDATE_SELECT:
    +            $("#select" + data.id).prop("disabled", !data.enabled);
    +            break;
    +
    +        case UI_BUTTON:
    +        case UPDATE_BUTTON:
    +            $("#btn" + data.id).prop("disabled", !data.enabled);
    +            break;
    +
    +        case UI_PAD:
    +        case UI_CPAD:
    +        case UPDATE_PAD:
    +        case UPDATE_CPAD:
    +            if (data.enabled) {
    +                $("#id" + data.id + " nav").removeClass('disabled');
    +            } else {
    +                $("#id" + data.id + " nav").addClass('disabled');
    +            }
    +            break;
    +    }
     }
    diff --git a/lib/ESPUI/data/js/controls.min.js b/lib/ESPUI/data/js/controls.min.js
    index e3b5774..3098429 100644
    --- a/lib/ESPUI/data/js/controls.min.js
    +++ b/lib/ESPUI/data/js/controls.min.js
    @@ -1,13 +1,16 @@
     const UI_INITIAL_GUI=200;const UI_RELOAD=201;const UPDATE_OFFSET=100;const UI_EXTEND_GUI=210;const UI_TITEL=0;const UI_PAD=1;const UPDATE_PAD=101;const UI_CPAD=2;const UPDATE_CPAD=102;const UI_BUTTON=3;const UPDATE_BUTTON=103;const UI_LABEL=4;const UPDATE_LABEL=104;const UI_SWITCHER=5;const UPDATE_SWITCHER=105;const UI_SLIDER=6;const UPDATE_SLIDER=106;const UI_NUMBER=7;const UPDATE_NUMBER=107;const UI_TEXT_INPUT=8;const UPDATE_TEXT_INPUT=108;const UI_GRAPH=9;const ADD_GRAPH_POINT=10;const CLEAR_GRAPH=109;const UI_TAB=11;const UPDATE_TAB=111;const UI_SELECT=12;const UPDATE_SELECT=112;const UI_OPTION=13;const UPDATE_OPTION=113;const UI_MIN=14;const UPDATE_MIN=114;const UI_MAX=15;const UPDATE_MAX=115;const UI_STEP=16;const UPDATE_STEP=116;const UI_GAUGE=17;const UPDATE_GAUGE=117;const UI_ACCEL=18;const UPDATE_ACCEL=118;const UI_SEPARATOR=19;const UPDATE_SEPARATOR=119;const UI_TIME=20;const UPDATE_TIME=120;const UP=0;const DOWN=1;const LEFT=2;const RIGHT=3;const CENTER=4;const C_TURQUOISE=0;const C_EMERALD=1;const C_PETERRIVER=2;const C_WETASPHALT=3;const C_SUNFLOWER=4;const C_CARROT=5;const C_ALIZARIN=6;const C_DARK=7;const C_NONE=255;var graphData=new Array();var hasAccel=false;var sliderContinuous=false;function colorClass(colorId){colorId=Number(colorId);switch(colorId){case C_TURQUOISE:return"turquoise";case C_EMERALD:return"emerald";case C_PETERRIVER:return"peterriver";case C_WETASPHALT:return"wetasphalt";case C_SUNFLOWER:return"sunflower";case C_CARROT:return"carrot";case C_ALIZARIN:return"alizarin";case C_DARK:case C_NONE:return"dark";default:return"";}}
    -var websock;var websockConnected=false;function requestOrientationPermission(){}
    +var websock;var websockConnected=false;var WebSocketTimer=null;function requestOrientationPermission(){}
     function saveGraphData(){localStorage.setItem("espuigraphs",JSON.stringify(graphData));}
     function restoreGraphData(id){var savedData=localStorage.getItem("espuigraphs",graphData);if(savedData!=null){savedData=JSON.parse(savedData);return savedData[id];}
     return[];}
    -function restart(){$(document).add("*").off();$("#row").html("");websock.close();start();}
    -function conStatusError(){websockConnected=false;$("#conStatus").removeClass("color-green");$("#conStatus").addClass("color-red");$("#conStatus").html("Error / No Connection ↻");$("#conStatus").off();$("#conStatus").on({click:restart,});}
    +function restart(){$(document).add("*").off();$("#row").html("");conStatusError();start();}
    +function conStatusError(){if(true===websockConnected){websockConnected=false;websock.close();$("#conStatus").removeClass("color-green");$("#conStatus").addClass("color-red");$("#conStatus").html("Error / No Connection ↻");$("#conStatus").off();$("#conStatus").on({click:restart,});}}
     function handleVisibilityChange(){if(!websockConnected&&!document.hidden){restart();}}
     function start(){document.addEventListener("visibilitychange",handleVisibilityChange,false);if(window.location.port!=""||window.location.port!=80||window.location.port!=443){websock=new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws");}else{websock=new WebSocket("ws://"+window.location.hostname+"/ws");}
    -websock.onopen=function(evt){console.log("websock open");$("#conStatus").addClass("color-green");$("#conStatus").text("Connected");websockConnected=true;};websock.onclose=function(evt){console.log("websock close");conStatusError();};websock.onerror=function(evt){console.log(evt);conStatusError();};var handleEvent=function(evt){console.log(evt);var data=JSON.parse(evt.data);var e=document.body;var center="";switch(data.type){case UI_INITIAL_GUI:$("#row").html("");$("#tabsnav").html("");$("#tabscontent").html("");if(data.sliderContinuous){sliderContinuous=data.sliderContinuous;}
    +if(null===WebSocketTimer){WebSocketTimer=setInterval(function(){if(websock.readyState===3){restart();}},5000);}
    +websock.onopen=function(evt){console.log("websock open");$("#conStatus").addClass("color-green");$("#conStatus").text("Connected");websockConnected=true;};websock.onclose=function(evt){console.log("websock close");conStatusError();};websock.onerror=function(evt){console.log("websock Error");console.log(evt);restart();};var handleEvent=function(evt){console.log(evt);try{var data=JSON.parse(evt.data);}
    +catch(Event){console.error(Event);websock.send("uiok:"+0);return;}
    +var e=document.body;var center="";switch(data.type){case UI_INITIAL_GUI:$("#row").html("");$("#tabsnav").html("");$("#tabscontent").html("");if(data.sliderContinuous){sliderContinuous=data.sliderContinuous;}
     data.controls.forEach(element=>{var fauxEvent={data:JSON.stringify(element),};handleEvent(fauxEvent);});if(data.totalcontrols>(data.controls.length-1)){websock.send("uiok:"+(data.controls.length-1));}
     break;case UI_EXTEND_GUI:data.controls.forEach(element=>{var fauxEvent={data:JSON.stringify(element),};handleEvent(fauxEvent);});if(data.totalcontrols>data.startindex+(data.controls.length-1)){websock.send("uiok:"+(data.startindex+(data.controls.length-1)));}
     break;case UI_RELOAD:window.location.reload();break;case UI_TITEL:document.title=data.label;$("#mainHeader").html(data.label);break;case UI_LABEL:case UI_NUMBER:case UI_TEXT_INPUT:case UI_SELECT:case UI_GAUGE:case UI_SEPARATOR:if(data.visible)addToHTML(data);break;case UI_BUTTON:if(data.visible){addToHTML(data);$("#btn"+data.id).on({touchstart:function(e){e.preventDefault();buttonclick(data.id,true);},touchend:function(e){e.preventDefault();buttonclick(data.id,false);},});}
    @@ -33,9 +36,10 @@ break;case ADD_GRAPH_POINT:var ts=Math.round(new Date().getTime()/1000);graphDat
     break;case UPDATE_LABEL:$("#l"+data.id).html(data.value);if(data.hasOwnProperty('elementStyle')){$("#l"+data.id).attr("style",data.elementStyle);}
     break;case UPDATE_SWITCHER:switcher(data.id,data.value=="0"?0:1);if(data.hasOwnProperty('elementStyle')){$("#sl"+data.id).attr("style",data.elementStyle);}
     break;case UPDATE_SLIDER:$("#sl"+data.id).attr("value",data.value)
    -slider_move($("#id"+data.id),data.value,"100",false);if(data.hasOwnProperty('elementStyle')){$("#sl"+data.id).attr("style",data.elementStyle);}
    +slider_move($("#sl"+data.id).parent().parent(),data.value,"100",false);if(data.hasOwnProperty('elementStyle')){$("#sl"+data.id).attr("style",data.elementStyle);}
     break;case UPDATE_NUMBER:$("#num"+data.id).val(data.value);if(data.hasOwnProperty('elementStyle')){$("#num"+data.id).attr("style",data.elementStyle);}
     break;case UPDATE_TEXT_INPUT:$("#text"+data.id).val(data.value);if(data.hasOwnProperty('elementStyle')){$("#text"+data.id).attr("style",data.elementStyle);}
    +if(data.hasOwnProperty('inputType')){$("#text"+data.id).attr("type",data.inputType);}
     break;case UPDATE_SELECT:$("#select"+data.id).val(data.value);if(data.hasOwnProperty('elementStyle')){$("#select"+data.id).attr("style",data.elementStyle);}
     break;case UPDATE_BUTTON:$("#btn"+data.id).val(data.value);$("#btn"+data.id).text(data.value);if(data.hasOwnProperty('elementStyle')){$("#btn"+data.id).attr("style",data.elementStyle);}
     break;case UPDATE_PAD:case UPDATE_CPAD:break;case UPDATE_GAUGE:$("#gauge"+data.id).val(data.value);if(data.hasOwnProperty('elementStyle')){$("#gauge"+data.id).attr("style",data.elementStyle);}
    @@ -65,7 +69,7 @@ elementHTML(data)+
     "
    "+data.label+"

    ";break;case UI_TIME:break;} parent.append(html);}else{var parent=$("#id"+data.parentControl);parent.append(elementHTML(data));}} var elementHTML=function(data){var id=data.id -var elementStyle=data.hasOwnProperty('elementStyle')?" style='"+data.elementStyle+"' ":"";switch(data.type){case UI_LABEL:return""+data.value+"";case UI_BUTTON:return"";case UI_SWITCHER:return"
    ";case UI_NUMBER:return"";case UI_TEXT_INPUT:return"";case UI_SELECT:return"";case UI_SELECT:return"";case UI_ACCEL:return"ACCEL // Not implemented fully!
    ";default:return"";}}
    diff --git a/lib/ESPUI/docs/ui_inputtypes.png b/lib/ESPUI/docs/ui_inputtypes.png
    new file mode 100644
    index 0000000..3ed67b6
    Binary files /dev/null and b/lib/ESPUI/docs/ui_inputtypes.png differ
    diff --git a/lib/ESPUI/keywords.txt b/lib/ESPUI/keywords.txt
    index 1ce2287..84cd7eb 100644
    --- a/lib/ESPUI/keywords.txt
    +++ b/lib/ESPUI/keywords.txt
    @@ -25,6 +25,7 @@ beginLITTLEFS	KEYWORD2
     print	KEYWORD2
     updateSwitcher	KEYWORD2
     updateSlider	KEYWORD2
    +captivePortal	LITERAL1
     
     #######################################
     # Instances (KEYWORD2)
    diff --git a/lib/ESPUI/library.json b/lib/ESPUI/library.json
    index 8125983..3d89a96 100644
    --- a/lib/ESPUI/library.json
    +++ b/lib/ESPUI/library.json
    @@ -1,6 +1,6 @@
     {
       "name": "ESPUI",
    -  "keywords": "espressif web interface iot control simple easy ui userinterface",
    +  "keywords": "espressif web interface iot easy ui",
       "description": "ESP32 and ESP8266 Web Interface Library",
       "repository": {
         "type": "git",
    @@ -31,7 +31,7 @@
           "frameworks": "arduino"
         }
       ],
    -  "version": "2.1.0",
    +  "version": "2.1.1",
       "frameworks": "arduino",
       "platforms": "*"
     }
    diff --git a/lib/ESPUI/library.properties b/lib/ESPUI/library.properties
    index a4d1139..b38bbc3 100644
    --- a/lib/ESPUI/library.properties
    +++ b/lib/ESPUI/library.properties
    @@ -1,5 +1,5 @@
     name=ESPUI
    -version=2.1.0
    +version=2.1.1
     author=Lukas Bachschwell
     maintainer=Lukas Bachschwell 
     sentence=ESP32 and ESP8266 Web Interface Library
    diff --git a/lib/ESPUI/pio_examples/gui.zip b/lib/ESPUI/pio_examples/gui.zip
    new file mode 100644
    index 0000000..afb0691
    Binary files /dev/null and b/lib/ESPUI/pio_examples/gui.zip differ
    diff --git a/lib/ESPUI/pio_examples/gui/gui.zip b/lib/ESPUI/pio_examples/gui/gui.zip
    deleted file mode 100644
    index 62cb700..0000000
    Binary files a/lib/ESPUI/pio_examples/gui/gui.zip and /dev/null differ
    diff --git a/lib/ESPUI/src/ESPUI.cpp b/lib/ESPUI/src/ESPUI.cpp
    index fb72ac8..b65998c 100644
    --- a/lib/ESPUI/src/ESPUI.cpp
    +++ b/lib/ESPUI/src/ESPUI.cpp
    @@ -1,4 +1,3 @@
    -// ESPUI.cpp: Modified for ESP32 LittleFS, TEB Jun-13-2022
     #include "ESPUI.h"
     
     #include 
    @@ -14,8 +13,6 @@
     #include "dataTabbedcontentJS.h"
     #include "dataZeptoJS.h"
     
    -uint16_t Control::idCounter = 1;
    -
     // ################# LITTLEFS functions
     #if defined(ESP32)
     void listDir(const char* dirname, uint8_t levels)
    @@ -27,11 +24,7 @@ void listDir(const char* dirname, uint8_t levels)
         }
     #endif
     
    -#if defined(ESP32)
    -    File root = LittleFS.open(dirname);
    -#else
         File root = LittleFS.open(dirname);
    -#endif
     
         if (!root)
         {
    @@ -115,19 +108,11 @@ void listDir(const char* dirname, uint8_t levels)
     
     void ESPUIClass::list()
     {
    -#if defined(ESP32)
         if (!LittleFS.begin())
         {
             Serial.println(F("LittleFS Mount Failed"));
             return;
         }
    -#else
    -    if (!LittleFS.begin())
    -    {
    -        Serial.println(F("LittleFS Mount Failed"));
    -        return;
    -    }
    -#endif
     
         listDir("/", 1);
     #if defined(ESP32)
    @@ -147,12 +132,7 @@ void ESPUIClass::list()
     
     void deleteFile(const char* path)
     {
    -#if defined(ESP32)
    -    bool exists = LittleFS.exists(path);
    -#else
         bool exists = LittleFS.exists(path);
    -#endif
    -
         if (!exists)
         {
     #if defined(DEBUG_ESPUI)
    @@ -172,11 +152,7 @@ void deleteFile(const char* path)
         }
     #endif
     
    -#if defined(ESP32)
    -    bool didRemove = LittleFS.remove(path);
    -#else
         bool didRemove = LittleFS.remove(path);
    -#endif
         if (didRemove)
         {
     #if defined(DEBUG_ESPUI)
    @@ -206,12 +182,7 @@ void writeFile(const char* path, const char* data)
         }
     #endif
     
    -#if defined(ESP32)
         File file = LittleFS.open(path, FILE_WRITE);
    -#else
    -    File file = LittleFS.open(path, FILE_WRITE);
    -#endif
    -
         if (!file)
         {
     #if defined(DEBUG_ESPUI)
    @@ -277,7 +248,7 @@ void ESPUIClass::prepareFileSystem()
         // this function should only be used once
     
     #if defined(DEBUG_ESPUI)
    -    if (this->verbosity)
    +    if (verbosity)
         {
             Serial.println(F("About to prepare filesystem..."));
         }
    @@ -289,7 +260,7 @@ void ESPUIClass::prepareFileSystem()
         if (!LittleFS.begin(true))
         {
     #if defined(DEBUG_ESPUI)
    -        if (this->verbosity)
    +        if (verbosity)
             {
                 Serial.println(F("LittleFS Mount Failed"));
             }
    @@ -299,7 +270,7 @@ void ESPUIClass::prepareFileSystem()
         }
     
     #if defined(DEBUG_ESPUI)
    -    if (this->verbosity)
    +    if (verbosity)
         {
             listDir("/", 1);
             Serial.println(F("LittleFS Mount ESP32 Done"));
    @@ -311,9 +282,9 @@ void ESPUIClass::prepareFileSystem()
         LittleFS.begin();
     
     #if defined(DEBUG_ESPUI)
    -    if (this->verbosity)
    +    if (verbosity)
         {
    -        Serial.println(F("LittleFS Mount ESP8266 Done"));
    +        Serial.println(F("LITTLEFS Mount ESP8266 Done"));
         }
     #endif
     
    @@ -331,7 +302,7 @@ void ESPUIClass::prepareFileSystem()
         deleteFile("/js/tabbedcontent.js");
     
     #if defined(DEBUG_ESPUI)
    -    if (this->verbosity)
    +    if (verbosity)
         {
             Serial.println(F("Cleanup done"));
         }
    @@ -351,7 +322,7 @@ void ESPUIClass::prepareFileSystem()
         writeFile("/js/tabbedcontent.js", JS_TABBEDCONTENT);
     
     #if defined(DEBUG_ESPUI)
    -    if (this->verbosity)
    +    if (verbosity)
         {
             Serial.println(F("Done Initializing filesystem :-)"));
         }
    @@ -360,7 +331,7 @@ void ESPUIClass::prepareFileSystem()
     #if defined(ESP32)
     
     #if defined(DEBUG_ESPUI)
    -    if (this->verbosity)
    +    if (verbosity)
         {
             listDir("/", 1);
         }
    @@ -368,251 +339,89 @@ void ESPUIClass::prepareFileSystem()
     
     #endif
     
    -#if defined(ESP32)
         LittleFS.end();
    -#else
    -    LittleFS.end();
    -#endif
     }
     
     // Handle Websockets Communication
    -void onWsEvent(
    -    AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
    +void ESPUIClass::onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
     {
    -    switch (type)
    -    {
    -    case WS_EVT_DISCONNECT: {
    -#if defined(DEBUG_ESPUI)
    -        if (ESPUI.verbosity)
    -        {
    -            Serial.print(F("Disconnected!\n"));
    -        }
    -#endif
    -
    -        break;
    -    }
    -
    -    case WS_EVT_PONG: {
    -#if defined(DEBUG_ESPUI)
    -        if (ESPUI.verbosity)
    -        {
    -            Serial.print(F("Received PONG!\n"));
    -        }
    -#endif
    +    // Serial.println(String("ESPUIClass::OnWsEvent: type: ") + String(type));
    +    RemoveToBeDeletedControls();
     
    -        break;
    -    }
    +    if(WS_EVT_DISCONNECT == type)
    +    {
    +        #if defined(DEBUG_ESPUI)
    +            if (verbosity)
    +            {
    +                Serial.println(F("WS_EVT_DISCONNECT"));
    +            }
    +        #endif
     
    -    case WS_EVT_ERROR: {
    -#if defined(DEBUG_ESPUI)
    -        if (ESPUI.verbosity)
    +        if(MapOfClients.end() != MapOfClients.find(client->id()))
             {
    -            Serial.print(F("WebSocket Error!\n"));
    +            // Serial.println("Delete client.");
    +            delete MapOfClients[client->id()];
    +            MapOfClients.erase(client->id());
             }
    -#endif
    -
    -        break;
         }
    -
    -    case WS_EVT_CONNECT: {
    -#if defined(DEBUG_ESPUI)
    -        if (ESPUI.verbosity)
    -        {
    -            Serial.print(F("Connected: "));
    -            Serial.println(client->id());
    -        }
    -#endif
    -
    -        ESPUI.jsonDom(0, client);
    -
    -#if defined(DEBUG_ESPUI)
    -        if (ESPUI.verbosity)
    +    else
    +    {
    +        if(MapOfClients.end() == MapOfClients.find(client->id()))
             {
    -            Serial.println(F("JSON Data Sent to Client!"));
    +            // Serial.println("ESPUIClass::OnWsEvent:Create new client.");
    +            MapOfClients[client->id()] = new ESPUIclient(client);
             }
    -#endif
    +        MapOfClients[client->id()]->onWsEvent(type, arg, data, len);
         }
    -    break;
    -
    -    case WS_EVT_DATA: {
    -        String msg = "";
    -        msg.reserve(len + 1);
     
    -        for (size_t i = 0; i < len; i++)
    -        {
    -            msg += (char)data[i];
    -        }
    -
    -        if (msg.startsWith(F("uiok:")))
    -        {
    -            int idx = msg.substring(msg.indexOf(':') + 1).toInt();
    -            ESPUI.jsonDom(idx, client);
    -        } else
    -        {
    -            uint16_t id = msg.substring(msg.lastIndexOf(':') + 1).toInt();
    +    ClearControlUpdateFlags();
     
    -    #if defined(DEBUG_ESPUI)
    -            if (ESPUI.verbosity >= Verbosity::VerboseJSON)
    -            {
    -                Serial.print(F("WS rec: "));
    -                Serial.println(msg);
    -                Serial.print(F("WS recognised ID: "));
    -                Serial.println(id);
    -            }
    -    #endif
    +    return;
    +}
     
    -            Control* c = ESPUI.getControl(id);
    +uint16_t ESPUIClass::addControl(ControlType type, const char* label)
    +{
    +    return addControl(type, label, String(""));
    +}
     
    -            if (c == nullptr)
    -            {
    -    #if defined(DEBUG_ESPUI)
    -                if (ESPUI.verbosity)
    -                {
    -                    Serial.print(F("No control found for ID "));
    -                    Serial.println(id);
    -                }
    -    #endif
    -
    -                return;
    -            }
    +uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value)
    +{
    +    return addControl(type, label, value, ControlColor::Turquoise);
    +}
     
    -            if (c->callback == nullptr)
    -            {
    -    #if defined(DEBUG_ESPUI)
    -                if (ESPUI.verbosity)
    -                {
    -                    Serial.print(F("No callback found for ID "));
    -                    Serial.println(id);
    -                }
    -    #endif
    -
    -                return;
    -            }
    +uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color)
    +{
    +    return addControl(type, label, value, color, Control::noParent);
    +}
     
    -            if (msg.startsWith(F("bdown:")))
    -            {
    -                c->callback(c, B_DOWN);
    -            }
    -            else if (msg.startsWith(F("bup:")))
    -            {
    -                c->callback(c, B_UP);
    -            }
    -            else if (msg.startsWith(F("pfdown:")))
    -            {
    -                c->callback(c, P_FOR_DOWN);
    -            }
    -            else if (msg.startsWith(F("pfup:")))
    -            {
    -                c->callback(c, P_FOR_UP);
    -            }
    -            else if (msg.startsWith(F("pldown:")))
    -            {
    -                c->callback(c, P_LEFT_DOWN);
    -            }
    -            else if (msg.startsWith(F("plup:")))
    -            {
    -                c->callback(c, P_LEFT_UP);
    -            }
    -            else if (msg.startsWith(F("prdown:")))
    -            {
    -                c->callback(c, P_RIGHT_DOWN);
    -            }
    -            else if (msg.startsWith(F("prup:")))
    -            {
    -                c->callback(c, P_RIGHT_UP);
    -            }
    -            else if (msg.startsWith(F("pbdown:")))
    -            {
    -                c->callback(c, P_BACK_DOWN);
    -            }
    -            else if (msg.startsWith(F("pbup:")))
    -            {
    -                c->callback(c, P_BACK_UP);
    -            }
    -            else if (msg.startsWith(F("pcdown:")))
    -            {
    -                c->callback(c, P_CENTER_DOWN);
    -            }
    -            else if (msg.startsWith(F("pcup:")))
    -            {
    -                c->callback(c, P_CENTER_UP);
    -            }
    -            else if (msg.startsWith(F("sactive:")))
    -            {
    -                c->value = "1";
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, S_ACTIVE);
    -            }
    -            else if (msg.startsWith(F("sinactive:")))
    -            {
    -                c->value = "0";
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, S_INACTIVE);
    -            }
    -            else if (msg.startsWith(F("slvalue:")))
    -            {
    -                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, SL_VALUE);
    -            }
    -            else if (msg.startsWith(F("nvalue:")))
    -            {
    -                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, N_VALUE);
    -            }
    -            else if (msg.startsWith(F("tvalue:")))
    -            {
    -                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, T_VALUE);
    -            }
    -            else if (msg.startsWith("tabvalue:"))
    -            {
    -                c->callback(c, client->id());
    -            }
    -            else if (msg.startsWith(F("svalue:")))
    -            {
    -                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, S_VALUE);
    -            }
    -            else if (msg.startsWith(F("time:")))
    -            {
    -                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
    -                ESPUI.updateControl(c, client->id());
    -                c->callback(c, TM_VALUE);
    -            }
    -            else
    -            {
    -    #if defined(DEBUG_ESPUI)
    -                if (ESPUI.verbosity)
    -                {
    -                    Serial.println(F("Malformated message from the websocket"));
    -                }
    -    #endif
    -            }
    -        }
    -    }
    -    break;
    +uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color, uint16_t parentControl)
    +{
    +    return addControl(type, label, value, color, parentControl, nullptr);
    +}
     
    -    default:
    -        break;
    -    }
    +uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color, uint16_t parentControl, void (*callback)(Control*, int))
    +{
    +    uint16_t id = addControl(type, label, value, color, parentControl, nullptr, nullptr);
    +    // set the original style callback
    +    getControl(id)->callback = callback;
    +    return id;
     }
     
    -uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color,
    -    uint16_t parentControl, void (*callback)(Control*, int))
    +uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color, uint16_t parentControl, void (*callback)(Control*, int, void *), void * UserData)
     {
    -    Control* control = new Control(type, label, callback, value, color, true, parentControl);
    +#ifdef ESP32
    +    xSemaphoreTake(ControlsSemaphore, portMAX_DELAY);
    +#endif // def ESP32
    +
    +    Control* control = new Control(type, label, callback, UserData, value, color, true, parentControl);
     
    -    if (this->controls == nullptr)
    +    if (controls == nullptr)
         {
    -        this->controls = control;
    +        controls = control;
         }
         else
         {
    -        Control* iterator = this->controls;
    +        Control* iterator = controls;
     
             while (iterator->next != nullptr)
             {
    @@ -622,58 +431,82 @@ uint16_t ESPUIClass::addControl(ControlType type, const char* label, const Strin
             iterator->next = control;
         }
     
    -    this->controlCount++;
    +    controlCount++;
    +
    +#ifdef ESP32
    +    xSemaphoreGive(ControlsSemaphore);
    +#endif // def ESP32
    +
    +    NotifyClients(ClientUpdateType_t::RebuildNeeded);
     
         return control->id;
     }
     
    -bool ESPUIClass::removeControl(uint16_t id, bool force_reload_ui)
    +bool ESPUIClass::removeControl(uint16_t id, bool force_rebuild_ui)
     {
    -    if (nullptr == this->controls)
    -        return false;
    -
    -    Control* it = this->controls;
    +    bool Response = false;
     
    -    if (id == it->id)
    +    Control* control = getControl(id);
    +    if (control)
         {
    -        this->controls = it->next;
    -        delete it;
    -        this->controlCount--;
    -        if (force_reload_ui)
    +        Response = true;
    +        control->DeleteControl();
    +        controlCount--;
    +
    +        if(force_rebuild_ui)
             {
                 jsonReload();
             }
             else
             {
    -            jsonDom(0);
    +            NotifyClients(ClientUpdateType_t::RebuildNeeded);
             }
    -        return true;
         }
    -
    -    Control* it_next = it->next;
    -    while (nullptr != it_next && id != it_next->id)
    +#ifdef DEBUG_ESPUI
    +    else
         {
    -        it = it_next;
    -        it_next = it_next->next;
    +        // Serial.println(String("Could not Remove Control ") + String(id));
         }
    +#endif // def DEBUG_ESPUI
     
    -    if (nullptr != it_next)
    +    return Response;
    +}
    +
    +void ESPUIClass::RemoveToBeDeletedControls()
    +{
    +    #ifdef ESP32
    +    xSemaphoreTake(ControlsSemaphore, portMAX_DELAY);
    +    #endif // def ESP32
    +
    +    Control* PreviousControl = nullptr;
    +    Control* CurrentControl = controls;
    +
    +    while (nullptr != CurrentControl)
         {
    -        it->next = it_next->next;
    -        delete it_next;
    -        this->controlCount--;
    -        if (force_reload_ui)
    +        Control* NextControl = CurrentControl->next;
    +        if (CurrentControl->ToBeDeleted())
             {
    -            jsonReload();
    +            if (CurrentControl == controls)
    +            {
    +                // this is the root control
    +                controls = NextControl;
    +            }
    +            else
    +            {
    +                PreviousControl->next = NextControl;
    +            }
    +            delete CurrentControl;
    +            CurrentControl = NextControl;
             }
             else
             {
    -            jsonDom(0); // resends to all
    +            PreviousControl = CurrentControl;
    +            CurrentControl = NextControl;
             }
    -        return true;
         }
    -
    -    return false;
    +    #ifdef ESP32
    +    xSemaphoreGive(ControlsSemaphore);
    +    #endif // def ESP32
     }
     
     uint16_t ESPUIClass::label(const char* label, ControlColor color, const String& value)
    @@ -686,10 +519,16 @@ uint16_t ESPUIClass::graph(const char* label, ControlColor color)
         return addControl(ControlType::Graph, label, "", color);
     }
     
    -uint16_t ESPUIClass::slider(
    -    const char* label, void (*callback)(Control*, int), ControlColor color, int value, int min, int max)
    +uint16_t ESPUIClass::slider(const char* label, void (*callback)(Control*, int), ControlColor color, int value, int min, int max)
    +{
    +    uint16_t id = slider(label, nullptr, color, value, min, max, nullptr);
    +    getControl(id)->callback = callback;
    +    return id;
    +}
    +
    +uint16_t ESPUIClass::slider(const char* label, void (*callback)(Control*, int, void*), ControlColor color, int value, int min, int max, void* userData)
     {
    -    uint16_t sliderId = addControl(ControlType::Slider, label, String(value), color, Control::noParent, callback);
    +    uint16_t sliderId = addControl(ControlType::Slider, label, String(value), color, Control::noParent, callback, userData);
         addControl(ControlType::Min, label, String(min), ControlColor::None, sliderId);
         addControl(ControlType::Max, label, String(max), ControlColor::None, sliderId);
     
    @@ -701,22 +540,42 @@ uint16_t ESPUIClass::button(const char* label, void (*callback)(Control*, int),
         return addControl(ControlType::Button, label, value, color, Control::noParent, callback);
     }
     
    +uint16_t ESPUIClass::button(const char* label, void (*callback)(Control*, int, void*), ControlColor color, const String& value, void* UserData)
    +{
    +    return addControl(ControlType::Button, label, value, color, Control::noParent, callback, UserData);
    +}
    +
     uint16_t ESPUIClass::switcher(const char* label, void (*callback)(Control*, int), ControlColor color, bool startState)
     {
         return addControl(ControlType::Switcher, label, startState ? "1" : "0", color, Control::noParent, callback);
     }
     
    +uint16_t ESPUIClass::switcher(const char* label, void (*callback)(Control*, int, void*), ControlColor color, bool startState, void* UserData)
    +{
    +    return addControl(ControlType::Switcher, label, startState ? "1" : "0", color, Control::noParent, callback, UserData);
    +}
    +
     uint16_t ESPUIClass::pad(const char* label, void (*callback)(Control*, int), ControlColor color)
     {
         return addControl(ControlType::Pad, label, "", color, Control::noParent, callback);
     }
    +
    +uint16_t ESPUIClass::pad(const char* label, void (*callback)(Control*, int, void*), ControlColor color, void* UserData)
    +{
    +    return addControl(ControlType::Pad, label, "", color, Control::noParent, callback, UserData);
    +}
    +
     uint16_t ESPUIClass::padWithCenter(const char* label, void (*callback)(Control*, int), ControlColor color)
     {
         return addControl(ControlType::PadWithCenter, label, "", color, Control::noParent, callback);
     }
     
    -uint16_t ESPUIClass::number(
    -    const char* label, void (*callback)(Control*, int), ControlColor color, int number, int min, int max)
    +uint16_t ESPUIClass::padWithCenter(const char* label, void (*callback)(Control*, int, void*), ControlColor color, void* UserData)
    +{
    +    return addControl(ControlType::PadWithCenter, label, "", color, Control::noParent, callback, UserData);
    +}
    +
    +uint16_t ESPUIClass::number(const char* label, void (*callback)(Control*, int), ControlColor color, int number, int min, int max)
     {
         uint16_t numberId = addControl(ControlType::Number, label, String(number), color, Control::noParent, callback);
         addControl(ControlType::Min, label, String(min), ControlColor::None, numberId);
    @@ -724,6 +583,14 @@ uint16_t ESPUIClass::number(
         return numberId;
     }
     
    +uint16_t ESPUIClass::number(const char* label, void (*callback)(Control*, int, void*), ControlColor color, int number, int min, int max, void* UserData)
    +{
    +    uint16_t numberId = addControl(ControlType::Number, label, String(number), color, Control::noParent, callback, UserData);
    +    addControl(ControlType::Min, label, String(min), ControlColor::None, numberId);
    +    addControl(ControlType::Max, label, String(max), ControlColor::None, numberId);
    +    return numberId;
    +}
    +
     uint16_t ESPUIClass::gauge(const char* label, ControlColor color, int number, int min, int max)
     {
         uint16_t numberId = addControl(ControlType::Gauge, label, String(number), color, Control::noParent);
    @@ -741,93 +608,66 @@ uint16_t ESPUIClass::accelerometer(const char* label, void (*callback)(Control*,
         return addControl(ControlType::Accel, label, "", color, Control::noParent, callback);
     }
     
    +uint16_t ESPUIClass::accelerometer(const char* label, void (*callback)(Control*, int, void*), ControlColor color, void* UserData)
    +{
    +    return addControl(ControlType::Accel, label, "", color, Control::noParent, callback, UserData);
    +}
    +
     uint16_t ESPUIClass::text(const char* label, void (*callback)(Control*, int), ControlColor color, const String& value)
     {
         return addControl(ControlType::Text, label, value, color, Control::noParent, callback);
     }
     
    +uint16_t ESPUIClass::text(const char* label, void (*callback)(Control*, int, void*), ControlColor color, const String& value, void* UserData)
    +{
    +    return addControl(ControlType::Text, label, value, color, Control::noParent, callback, UserData);
    +}
    +
     Control* ESPUIClass::getControl(uint16_t id)
     {
    -    Control* control = this->controls;
    +#ifdef ESP32
    +    xSemaphoreTake(ControlsSemaphore, portMAX_DELAY);
    +    Control* Response = getControlNoLock(id);
    +    xSemaphoreGive(ControlsSemaphore);
    +    return Response;
    +#else
    +    return getControlNoLock(id);
    +#endif // !def ESP32
    +}
    +
    +// WARNING: Anytime you walk the chain of controllers, the protection semaphore
    +//          MUST be locked. This function assumes that the semaphore is locked
    +//          at the time it is called. Make sure YOU locked it :)
    +Control* ESPUIClass::getControlNoLock(uint16_t id)
    +{
    +    Control* Response = nullptr;
    +    Control* control = controls;
     
    -    while (control != nullptr)
    +    while (nullptr != control)
         {
             if (control->id == id)
             {
    -            return control;
    +            if(!control->ToBeDeleted())
    +            {
    +                Response = control;
    +            }
    +            break;
             }
    -
             control = control->next;
         }
     
    -    return nullptr;
    +    return Response;
     }
     
    -void ESPUIClass::updateControl(Control* control, int clientId)
    +void ESPUIClass::updateControl(Control* control, int)
     {
         if (!control)
         {
             return;
         }
    -
    -    if (this->ws == nullptr)
    -    {
    -        return;
    -    }
    -
    -    String json;
    -    DynamicJsonDocument document(jsonUpdateDocumentSize);
    -    JsonObject root = document.to();
    -
    -    root["type"] = (int)control->type + ControlType::UpdateOffset;
    -    root["value"] = control->value;
    -    root["id"] = control->id;
    -    root["visible"] = control->visible;
    -    root["color"] = (int)control->color;
    -    root["enabled"] = control->enabled;
    -    if (control->panelStyle.length())
    -        root["panelStyle"] = control->panelStyle;
    -    if (control->elementStyle.length())
    -        root["elementStyle"] = control->elementStyle;
    -    serializeJson(document, json);
    -
    -#if defined(DEBUG_ESPUI)
    -    if (this->verbosity >= Verbosity::VerboseJSON)
    -    {
    -        Serial.println(json);
    -    }
    -#endif
    -
    -    if (clientId < 0)
    -    {
    -#if defined(DEBUG_ESPUI)
    -        if (this->verbosity >= Verbosity::VerboseJSON)
    -        {
    -            Serial.println(F("TextAll"));
    -        }
    -#endif
    -        this->ws->textAll(json);
    -        return;
    -    }
    -    // This is a hacky workaround because ESPAsyncWebServer does not have a
    -    // function like this and it's clients array is private
    -    int tryId = 0;
    -
    -    //for (size_t count = 0; tryId < (int)this->ws->count() && count < this->ws->count();)
    -    for (size_t count = 0; count < this->ws->count();)
    -    {
    -        if (this->ws->hasClient(tryId))
    -        {
    -            if (clientId != tryId)
    -            {
    -                this->ws->client(tryId)->text(json);
    -            }
    -
    -            count++;
    -        }
    -
    -        tryId++;
    -    }
    +    // tel the control it has been updated
    +    control->HasBeenUpdated();
    +    NotifyClients(ClientUpdateType_t::UpdateNeeded);
     }
     
     void ESPUIClass::setPanelStyle(uint16_t id, String style, int clientId)
    @@ -850,7 +690,18 @@ void ESPUIClass::setElementStyle(uint16_t id, String style, int clientId)
         }
     }
     
    -void ESPUIClass::setPanelWide(uint16_t id, bool wide) {
    +void ESPUIClass::setInputType(uint16_t id, String type, int clientId)
    +{
    +    Control* control = getControl(id);
    +    if (control)
    +    {
    +        control->inputType = type;
    +        updateControl(control, clientId);
    +    }
    +}
    +
    +void ESPUIClass::setPanelWide(uint16_t id, bool wide)
    +{
         Control* control = getControl(id);
         if (control)
         {
    @@ -862,12 +713,12 @@ void ESPUIClass::setEnabled(uint16_t id, bool enabled, int clientId) {
         Control* control = getControl(id);
         if (control)
         {
    +        // Serial.println(String("CreateAllowed: id: ") + String(clientId) + " State: " + String(enabled));
             control->enabled = enabled;
             updateControl(control, clientId);
         }
     }
     
    -
     void ESPUIClass::setVertical(uint16_t id, bool vert) {
         Control* control = getControl(id);
         if (control)
    @@ -883,9 +734,9 @@ void ESPUIClass::updateControl(uint16_t id, int clientId)
         if (!control)
         {
     #if defined(DEBUG_ESPUI)
    -        if (this->verbosity)
    +        if (verbosity)
             {
    -            Serial.printf_P(PSTR("Error: There is no control with ID %d"), id);
    +            Serial.printf_P(PSTR("Error: Update Control: There is no control with ID %d\n"), id);
             }
     #endif
             return;
    @@ -912,9 +763,9 @@ void ESPUIClass::updateControlValue(uint16_t id, const String& value, int client
         if (!control)
         {
     #if defined(DEBUG_ESPUI)
    -        if (this->verbosity)
    +        if (verbosity)
             {
    -            Serial.printf_P(PSTR("Error: There is no control with ID %d"), id);
    +            Serial.printf_P(PSTR("Error: updateControlValue Control: There is no control with ID %d\n"), id);
             }
     #endif
             return;
    @@ -923,12 +774,33 @@ void ESPUIClass::updateControlValue(uint16_t id, const String& value, int client
         updateControlValue(control, value, clientId);
     }
     
    +void ESPUIClass::updateControlLabel(uint16_t id, const char * value, int clientId)
    +{
    +    updateControlLabel(getControl(id), value, clientId);
    +}
    +
    +void ESPUIClass::updateControlLabel(Control* control, const char * value, int clientId)
    +{
    +    if (!control)
    +    {
    +#if defined(DEBUG_ESPUI)
    +        if (verbosity)
    +        {
    +            Serial.printf_P(PSTR("Error: updateControlLabel Control: There is no control with the requested ID \n"));
    +        }
    +#endif
    +        return;
    +    }
    +    control->label = value;
    +    updateControl(control, clientId);
    +}
    +
     void ESPUIClass::updateVisibility(uint16_t id, bool visibility, int clientId) {
         Control* control = getControl(id);
         if(control)
         {
             control->visible = visibility;
    -        updateControl(id);
    +        updateControl(control, clientId);
         }
     }
     
    @@ -985,195 +857,93 @@ void ESPUIClass::clearGraph(uint16_t id, int clientId) { }
     
     void ESPUIClass::addGraphPoint(uint16_t id, int nValue, int clientId)
     {
    -    Control* control = getControl(id);
    -    if (!control)
    +    do // once
         {
    -        return;
    -    }
    +        Control* control = getControl(id);
    +        if (!control)
    +        {
    +            break;
    +        }
     
    -    String json;
    -    DynamicJsonDocument document(jsonUpdateDocumentSize);
    -    JsonObject root = document.to();
    +        DynamicJsonDocument document(jsonUpdateDocumentSize);
    +        JsonObject root = document.to();
     
    -    root["type"] = (int)ControlType::GraphPoint;
    -    root["value"] = nValue;
    -    root["id"] = control->id;
    -    serializeJson(document, json);
    +        root[F("type")] = (int)ControlType::GraphPoint;
    +        root[F("value")] = nValue;
    +        root[F("id")] = control->id;
     
    -#if defined(DEBUG_ESPUI)
    -    if (this->verbosity >= Verbosity::VerboseJSON)
    -    {
    -        Serial.println(json);
    -    }
    -#endif
    +        SendJsonDocToWebSocket(document, clientId);
     
    -    if (clientId < 0)
    -    {
    -        this->ws->textAll(json);
    -        return;
    -    }
    -    // This is a hacky workaround because ESPAsyncWebServer does not have a
    -    // function like this and it's clients array is private
    -    int tryId = 0;
    +    } while(false);
    +}
    +
    +bool ESPUIClass::SendJsonDocToWebSocket(ArduinoJson::DynamicJsonDocument& document, uint16_t clientId)
    +{
    +    bool Response = false;
     
    -    //for (size_t count = 0; tryId < (int)this->ws->count() && count < this->ws->count();)
    -    for (size_t count = 0; count < this->ws->count();)
    +    if(0 > clientId)
         {
    -        if (this->ws->hasClient(tryId))
    +        if(MapOfClients.end() != MapOfClients.find(clientId))
             {
    -            if (clientId != tryId)
    -            {
    -                this->ws->client(tryId)->text(json);
    -
    -#if defined(DEBUG_ESPUI)
    -                if (this->verbosity >= Verbosity::VerboseJSON)
    -                {
    -                    Serial.println(json);
    -                }
    -#endif
    -            }
    -
    -            count++;
    +            Response = MapOfClients[clientId]->SendJsonDocToWebSocket(document);
             }
    -
    -        tryId++;
         }
    -}
    -
    -/*
    -Convert & Transfer Arduino elements to JSON elements. This function sends a chunk of
    -JSON describing the controls of the UI, starting from the control at index startidx.
    -If startidx is 0 then a UI_INITIAL_GUI message will be sent, else a UI_EXTEND_GUI.
    -Both message types contain a list of serialised UI elements. Only a portion of the UI
    -will be sent in order to avoid websocket buffer overflows. The client will acknowledge
    -receipt of a partial message by requesting the next chunk of UI.
    -
    -The protocol is:
    -SERVER: jsonDom(0):
    -    "UI_INITIAL_GUI: n serialised UI elements"
    -CLIENT: controls.js:handleEvent()
    -    "uiok:n"
    -SERVER: jsonDom(n):
    -    "UI_EXTEND_GUI: n serialised UI elements"
    -CLIENT: controls.js:handleEvent()
    -    "uiok:2*n"
    -etc.
    -*/
    -void ESPUIClass::jsonDom(uint16_t startidx, AsyncWebSocketClient* client)
    -{
    -    if(startidx >= this->controlCount)
    +    else
         {
    -        return;
    +        for(auto CurrentClient : MapOfClients)
    +        {
    +            Response |= CurrentClient.second->SendJsonDocToWebSocket(document);
    +        }
         }
     
    -    DynamicJsonDocument document(jsonInitialDocumentSize);
    -    document["type"] = startidx ? (int)UI_EXTEND_GUI : (int)UI_INITIAL_GUI;
    -    document["sliderContinuous"] = sliderContinuous;
    -    document["startindex"] = startidx;
    -    document["totalcontrols"] = this->controlCount;
    -    JsonArray items = document.createNestedArray("controls");
    -    JsonObject titleItem = items.createNestedObject();
    -    titleItem["type"] = (int)UI_TITLE;
    -    titleItem["label"] = ui_title;
    -
    -    prepareJSONChunk(client, startidx, &items);
    +    return Response;
    +}
     
    -    String json;
    -    serializeJson(document, json);
    -#if defined(DEBUG_ESPUI)
    -    if (this->verbosity >= Verbosity::VerboseJSON)
    -    {
    -        Serial.println("Sending elements --------->");
    -        Serial.println(json);
    -    }
    -#endif
    -    if (client != nullptr)
    -        client->text(json);
    -    else
    -        this->ws->textAll(json);
    +void ESPUIClass::jsonDom(uint16_t, AsyncWebSocketClient*, bool)
    +{
    +    NotifyClients(ClientUpdateType_t::RebuildNeeded);
     }
     
    -/* Prepare a chunk of elements as a single JSON string. If the allowed number of elements is greater than the total
    -number this will represent the entire UI. More likely, it will represent a small section of the UI to be sent. The client
    -will acknoledge receipt by requesting the next chunk. */
    -void ESPUIClass::prepareJSONChunk(AsyncWebSocketClient* client, uint16_t startindex, JsonArray* items)
    +// Tell all of the clients that they need to ask for an upload of the control data.
    +void ESPUIClass::NotifyClients(ClientUpdateType_t newState)
     {
    -    //First check that there will be sufficient nodes in the list
    -    if(startindex >= this->controlCount)
    +    for (auto& CurrentClient : MapOfClients)
         {
    -        return;
    +        CurrentClient.second->NotifyClient(newState);
         }
    +}
     
    -    //Follow the list until control points to the startindex'th node
    -    Control* control = this->controls;
    -    for(auto i = 0; i < startindex; i++) {
    -        control = control->next;
    -    }
    +void ESPUIClass::ClearControlUpdateFlags()
    +{
    +    bool CanClearUpdateFlags = true;
     
    -    //To prevent overflow, keep track of the number of elements we have serialised into this message
    -    int elementcount = 0;
    -    while (control != nullptr && elementcount < 10)
    +    for(auto& CurrentClient : MapOfClients)
         {
    -        JsonObject item = items->createNestedObject();
    -
    -        item["id"] = String(control->id);
    -        item["type"] = (int)control->type;
    -        item["label"] = control->label;
    -        item["value"] = String(control->value);
    -        item["color"] = (int)control->color;
    -        item["visible"] = (int)control->visible;
    -        item["enabled"] = control->enabled;
    -        if (control->panelStyle.length())
    -            item["panelStyle"] = String(control->panelStyle);
    -        if (control->elementStyle.length())
    -            item["elementStyle"] = String(control->elementStyle);
    -        if (control->wide == true)
    -            item["wide"] = true;
    -        if (control->vertical == true)
    -            item["vertical"] = true;
    -
    -        if (control->parentControl != Control::noParent)
    +        if(!CurrentClient.second->IsSyncronized())
             {
    -            item["parentControl"] = String(control->parentControl);
    +            CanClearUpdateFlags = false;
    +            break;
             }
    +    }
     
    -        // special case for selects: to preselect an option, you have to add
    -        // "selected" to 
    ";break;case UI_TIME:break;} parent.append(html);}else{var parent=$("#id"+data.parentControl);parent.append(elementHTML(data));}} var elementHTML=function(data){var id=data.id -var elementStyle=data.hasOwnProperty('elementStyle')?" style='"+data.elementStyle+"' ":"";switch(data.type){case UI_LABEL:return""+data.value+"";case UI_BUTTON:return"";case UI_SWITCHER:return"