From fdffb9c04153f293822aa9ef291a6fc5eda6d618 Mon Sep 17 00:00:00 2001 From: Martin Mueller Date: Wed, 21 Sep 2022 15:33:05 -0400 Subject: [PATCH 1/7] Added a check for an invalid json format to prevent the UI from crashing. Requests a new reload when the issue is encountered. --- data/js/controls.js | 13 ++- data/js/controls.min.js | 210 ++++++++++++++++++++-------------------- 2 files changed, 119 insertions(+), 104 deletions(-) diff --git a/data/js/controls.js b/data/js/controls.js index 7a436c0..2db30eb 100644 --- a/data/js/controls.js +++ b/data/js/controls.js @@ -230,13 +230,24 @@ function start() { }; websock.onerror = function (evt) { + console.log("websock Error"); console.log(evt); conStatusError(); }; var handleEvent = function (evt) { console.log(evt); - var data = JSON.parse(evt.data); + try + { + var data = JSON.parse(evt.data); + } + catch (Event) + { + console.error(Event); + // start the update over again + websock.send("uiok:" + 0); + return; + } var e = document.body; var center = ""; diff --git a/data/js/controls.min.js b/data/js/controls.min.js index 1e7dd13..e18bcce 100644 --- a/data/js/controls.min.js +++ b/data/js/controls.min.js @@ -1,104 +1,108 @@ -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(){} -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 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;} -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);},});} -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: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");$("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){if($('#sl'+data.parentControl).length){$('#sl'+data.parentControl).attr("min",data.value);}else if($('#num'+data.parentControl).length){$('#num'+data.parentControl).attr("min",data.value);}} -break;case UI_MAX:if(data.parentControl){if($('#sl'+data.parentControl).length){$('#sl'+data.parentControl).attr("max",data.value);}else if($('#text'+data.parentControl).length){$('#text'+data.parentControl).attr("maxlength",data.value);}else if($('#num'+data.parentControl).length){$('#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;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;} -if(data.type>=UI_TITEL&&data.type=UPDATE_OFFSET&&data.type0){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: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+"' ":"";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){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');}
    +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(){}
    +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 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("websock Error");console.log(evt);conStatusError();};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);},});}
    +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: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");$("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){if($('#sl'+data.parentControl).length){$('#sl'+data.parentControl).attr("min",data.value);}else if($('#num'+data.parentControl).length){$('#num'+data.parentControl).attr("min",data.value);}} +break;case UI_MAX:if(data.parentControl){if($('#sl'+data.parentControl).length){$('#sl'+data.parentControl).attr("max",data.value);}else if($('#text'+data.parentControl).length){$('#text'+data.parentControl).attr("maxlength",data.value);}else if($('#num'+data.parentControl).length){$('#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;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;} +if(data.type>=UI_TITEL&&data.type=UPDATE_OFFSET&&data.type0){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: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+"' ":"";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){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;}}
    \ No newline at end of file
    
    From a2923e501f78c32d10f32fd2b0833a184cbbcffa Mon Sep 17 00:00:00 2001
    From: Martin Mueller 
    Date: Wed, 21 Sep 2022 15:37:20 -0400
    Subject: [PATCH 2/7] Major restructuring of the code to get past an issue in
     the web socket where many changes are requested but the WS API cant handle
     the requests. Requests are now tracked per UI client. Moved the client out of
     the main code into its own code and moved control data marshaling into the
     control class. NONE of these changes impact the users API. No code changes
     are needed by the users. WARNING: The LittleFS support for the ESP32 has been
     updated to be compatible with the latest ESP32 infrastructure. This includes
     using an improved WebServer.
    
    ---
     src/ESPUIclient.cpp    | 491 +++++++++++++++++++++++++++++++++++++++++
     src/ESPUIclient.h      |  62 ++++++
     src/ESPUIclientFsm.cpp | 107 +++++++++
     src/ESPUIclientFsm.h   |  79 +++++++
     src/ESPUIcontrol.cpp   | 251 +++++++++++++++++++++
     src/ESPUIcontrol.h     | 132 +++++++++++
     6 files changed, 1122 insertions(+)
     create mode 100644 src/ESPUIclient.cpp
     create mode 100644 src/ESPUIclient.h
     create mode 100644 src/ESPUIclientFsm.cpp
     create mode 100644 src/ESPUIclientFsm.h
     create mode 100644 src/ESPUIcontrol.cpp
     create mode 100644 src/ESPUIcontrol.h
    
    diff --git a/src/ESPUIclient.cpp b/src/ESPUIclient.cpp
    new file mode 100644
    index 0000000..adc0e5d
    --- /dev/null
    +++ b/src/ESPUIclient.cpp
    @@ -0,0 +1,491 @@
    +#include "ESPUI.h"
    +#include "ESPUIclient.h"
    +#include "ESPUIcontrol.h"
    +
    +ESPUIclient::ESPUIclient(AsyncWebSocketClient * _client):
    +    client(_client)
    +{
    +    fsm_EspuiClient_state_Idle_imp.SetParent(this);
    +    fsm_EspuiClient_state_SendingUpdate_imp.SetParent(this);
    +    fsm_EspuiClient_state_Rebuilding_imp.SetParent(this);
    +    fsm_EspuiClient_state_Reloading_imp.SetParent(this);
    +
    +    fsm_EspuiClient_state_Idle_imp.Init();
    +}
    +
    +ESPUIclient::ESPUIclient(const ESPUIclient& source):
    +    client(source.client)
    +{
    +    fsm_EspuiClient_state_Idle_imp.SetParent(this);
    +    fsm_EspuiClient_state_SendingUpdate_imp.SetParent(this);
    +    fsm_EspuiClient_state_Rebuilding_imp.SetParent(this);
    +    fsm_EspuiClient_state_Reloading_imp.SetParent(this);
    +
    +    fsm_EspuiClient_state_Idle_imp.Init();
    +}
    +
    +ESPUIclient::~ESPUIclient()
    +{
    +}
    +
    +bool ESPUIclient::CanSend()
    +{
    +    bool Response = false;
    +    if (nullptr != client)
    +    {
    +        Response = client->canSend();
    +    }
    +    return Response;
    +}
    +
    +void ESPUIclient::FillInHeader(DynamicJsonDocument& document)
    +{
    +    document[F("type")] = UI_EXTEND_GUI;
    +    document[F("sliderContinuous")] = ESPUI.sliderContinuous;
    +    document[F("startindex")] = 0;
    +    document[F("totalcontrols")] = ESPUI.controlCount;
    +    JsonArray items = document.createNestedArray(F("controls"));
    +    JsonObject titleItem = items.createNestedObject();
    +    titleItem[F("type")] = (int)UI_TITLE;
    +    titleItem[F("label")] = ESPUI.ui_title;
    +}
    +
    +bool ESPUIclient::IsSyncronized()
    +{
    +    return ((ClientUpdateType_t::Synchronized == ClientUpdateType) &&
    +            (&fsm_EspuiClient_state_Idle_imp == pCurrentFsmState));
    +}
    +
    +bool ESPUIclient::SendClientNotification(ClientUpdateType_t value)
    +{
    +    bool Response = false;
    +
    +    do // once
    +    {
    +        if(!CanSend())
    +        {
    +            // Serial.println(F("ESPUIclient::NotifyClient"));
    +            break;
    +        }
    +
    +        DynamicJsonDocument document(ESPUI.jsonUpdateDocumentSize);
    +        FillInHeader(document);
    +        if(ClientUpdateType_t::ReloadNeeded == value)
    +        {
    +            // Serial.println(F("ESPUIclient::SendClientNotification:set type to reload"));
    +            document["type"] = int(UI_RELOAD);
    +        }
    +        // dont send any controls
    +
    +        Response = SendJsonDocToWebSocket(document);
    +        // Serial.println(String("ESPUIclient::SendClientNotification:NotificationSent:Response: ") + String(Response));
    +
    +    } while (false);
    +    return Response;
    +}
    +
    +void ESPUIclient::NotifyClient(ClientUpdateType_t newState)
    +{
    +    SetState(newState);
    +    pCurrentFsmState->NotifyClient();
    +
    +#ifdef OldWay
    +    do // once
    +    {
    +        // Serial.println(String("ESPUIclient::NotifyClient: State: ") + String(int(newState)));
    +        SetState(newState);
    +
    +        if (HasBeenNotified)
    +        {
    +            // do not need to do anything
    +            break;
    +        }
    +        
    +        if(TransferIsInprogress)
    +        {
    +            // record that a notification was needed while we were transfering data to the client
    +            DelayedNotification = true;
    +            break;
    +        }
    +
    +        DelayedNotification = false;
    +
    +        if (SendJsonDocToWebSocket(document))
    +        {
    +            HasBeenNotified = true;
    +        }
    +
    +    } while (false);
    +
    +    return HasBeenNotified;
    +#endif // def OldWay
    +}
    +
    +// Handle Websockets Communication
    +void ESPUIclient::onWsEvent(AwsEventType type, void* arg, uint8_t* data, size_t len)
    +{
    +    // Serial.println(String("ESPUIclient::OnWsEvent: type: ") + String(type));
    +
    +    switch (type)
    +    {
    +        case WS_EVT_PONG: 
    +        {
    +            #if defined(DEBUG_ESPUI)
    +            if (ESPUI.verbosity)
    +            {
    +                Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_PONG"));
    +            }
    +            #endif
    +            break;
    +        }
    +
    +        case WS_EVT_ERROR: 
    +        {
    +            #if defined(DEBUG_ESPUI)
    +            if (ESPUI.verbosity)
    +            {
    +                Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_ERROR"));
    +            }
    +            #endif
    +            break;
    +        }
    +
    +        case WS_EVT_CONNECT:
    +        {
    +    		#if defined(DEBUG_ESPUI)
    +            if (ESPUI.verbosity)
    +            {
    +                Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_CONNECT"));
    +                Serial.println(client->id());
    +            }
    +    		#endif
    +
    +            // Serial.println("ESPUIclient:onWsEvent:WS_EVT_CONNECT: Call NotifyClient: RebuildNeeded");
    +            NotifyClient(ClientUpdateType_t::RebuildNeeded);
    +            break;
    +        }
    +
    +        case WS_EVT_DATA: 
    +        {
    +            // Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_DATA"));
    +            String msg = "";
    +            msg.reserve(len + 1);
    +
    +            for (size_t i = 0; i < len; i++)
    +            {
    +                msg += (char)data[i];
    +            }
    +
    +            String   cmd   = msg.substring(0, msg.indexOf(":"));
    +            String   value = msg.substring(cmd.length() + 1, msg.lastIndexOf(':'));
    +            uint16_t id    = msg.substring(msg.lastIndexOf(':') + 1).toInt();
    +
    +            #if defined(DEBUG_ESPUI)
    +                if (ESPUI.verbosity >= Verbosity::VerboseJSON)
    +                {
    +                    Serial.println(String(F("  WS msg: ")) + msg);
    +                    Serial.println(String(F("  WS cmd: ")) + cmd);
    +                    Serial.println(String(F("   WS id: ")) + String(id));
    +                    Serial.println(String(F("WS value: ")) + String(value));
    +                }
    +            #endif
    +
    +            if (cmd.equals(F("uiok")))
    +            {
    +                // Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_DATA:uiok:ProcessAck"));
    +                pCurrentFsmState->ProcessAck(id);
    +                break;
    +            }
    +
    +            if (cmd.equals(F("uiuok")))
    +            {
    +                // Serial.println(F("WS_EVT_DATA: uiuok. Unlock new async notifications"));
    +                break;
    +            }
    +
    +            Control* control = ESPUI.getControl(id);
    +            if (nullptr == control)
    +            {
    +                #if defined(DEBUG_ESPUI)
    +                if (ESPUI.verbosity)
    +                {
    +                    Serial.println(String(F("No control found for ID ")) + String(id));
    +                }
    +                #endif
    +                break;
    +            }
    +            control->onWsEvent(cmd, value);
    +            break;
    +        }
    +
    +        default:
    +        {
    +            // Serial.println(F("ESPUIclient::OnWsEvent:default"));
    +            break;
    +        }
    +    } // end switch
    +}
    +
    +/* 
    +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 acknowledge receipt by requesting the next chunk.
    + */
    +uint32_t ESPUIclient::prepareJSONChunk(uint16_t startindex, 
    +                                      DynamicJsonDocument & rootDoc, 
    +                                      bool InUpdateMode)
    +{
    +#ifdef ESP32
    +    xSemaphoreTake(ESPUI.ControlsSemaphore, portMAX_DELAY);
    +#endif // def ESP32
    +
    +    // Serial.println(String("prepareJSONChunk: Start. InUpdateMode: ") + String(InUpdateMode));
    +    int elementcount = 0;
    +
    +    do // once
    +    {
    +        // Follow the list until control points to the startindex'th node
    +        Control* control = ESPUI.controls;
    +        uint32_t currentIndex = 0;
    +        JsonArray items = rootDoc[F("controls")];
    +
    +        while ((startindex > currentIndex) && (nullptr != control))
    +        {
    +            // only count active controls
    +            if (!control->ToBeDeleted())
    +            {
    +                if(InUpdateMode)
    +                {
    +                    // In update mode we only count the controls that have been updated.
    +                    if(control->IsUpdated())
    +                    {
    +                        ++currentIndex;
    +                    }
    +                }
    +                else
    +                {
    +                    // not in update mode. Count all active controls
    +                    ++currentIndex;
    +                }
    +            }
    +            control = control->next;
    +        }
    +
    +        // any controls left to be processed?
    +        if(nullptr == control)
    +        {
    +            // Serial.println("prepareJSONChunk: No controls to process");
    +            break;
    +        }
    +
    +        // keep track of the number of elements we have serialised into this 
    +        // message. Overflow is detected and handled later in this loop 
    +        // and needs an index to the last item added.
    +        while (nullptr != control)
    +        {
    +            // skip deleted controls or controls that have not been updated
    +            if (control->ToBeDeleted())
    +            {
    +                // Serial.println(String("prepareJSONChunk: Ignoring Deleted control: ") + String(control->id));
    +                control = control->next;
    +                continue;
    +            }
    +
    +            if(InUpdateMode)
    +            {
    +                if(control->IsUpdated())
    +                {
    +                    // dont skip this control
    +                }
    +                else
    +                {
    +                    // control has not been updated. Skip it
    +                    control = control->next;
    +                    continue;
    +                }
    +            } 
    +
    +            JsonObject item = items.createNestedObject();
    +            elementcount++;
    +            control->MarshalControl(item, InUpdateMode);
    +            
    +            if (rootDoc.overflowed())
    +            {
    +                // String("prepareJSONChunk: too much data in the message. Remove the last entry");
    +                if (1 == elementcount)
    +                {
    +                    Serial.println(String(F("ERROR: prepareJSONChunk: Control ")) + String(control->id) + F(" is too large to be sent to the browser."));
    +                    rootDoc.clear();
    +                    item = items.createNestedObject();
    +                    control->MarshalErrorMessage(item);
    +                    elementcount = 0;
    +                }
    +                else
    +                {
    +                    // Serial.println(String("prepareJSONChunk: Defering control: ") + String(control->id));
    +                    // Serial.println(String("prepareJSONChunk: elementcount: ") + String(elementcount));
    +
    +                    items.remove(elementcount);
    +                    --elementcount;
    +                }
    +                // exit the loop
    +                control = nullptr;
    +
    +            }
    +            else
    +            {
    +                control = control->next;
    +            }
    +        } // end while (control != nullptr)
    +
    +    } while (false);
    +
    +#ifdef ESP32
    +    xSemaphoreGive(ESPUI.ControlsSemaphore);
    +#endif // def ESP32
    +
    +    // Serial.println(String("prepareJSONChunk: elementcount: ") + String(elementcount));
    +    return elementcount;
    +}
    +
    +/*
    +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: SendControlsToClient(0): 
    +    "UI_INITIAL_GUI: n serialised UI elements"
    +CLIENT: controls.js:handleEvent()
    +    "uiok:n"
    +SERVER: SendControlsToClient(n):
    +    "UI_EXTEND_GUI: n serialised UI elements"
    +CLIENT: controls.js:handleEvent()
    +    "uiok:2*n"
    +etc. 
    +    Returns true if all controls have been sent (aka: Done)
    +*/
    +bool ESPUIclient::SendControlsToClient(uint16_t startidx, 
    +                                       ClientUpdateType_t TransferMode)
    +{
    +    bool Response = false;
    +    // Serial.println(String("ESPUIclient:SendControlsToClient:startidx: ") + String(startidx));
    +    do // once
    +    {
    +        if(!CanSend())
    +        {
    +            // Serial.println("ESPUIclient:SendControlsToClient: Cannot Send to clients.");
    +            break;
    +        }
    +
    +        if (startidx >= ESPUI.controlCount)
    +        {
    +            // Serial.println("ESPUIclient:SendControlsToClient: No more controls to send.");
    +            Response = true;
    +            break;
    +        }
    +
    +        DynamicJsonDocument document(ESPUI.jsonInitialDocumentSize);
    +        FillInHeader(document);
    +        document[F("startindex")] = startidx;
    +        document[F("totalcontrols")] = 65534; // ESPUI.controlCount;
    +
    +        if(0 == startidx)
    +        {
    +            // Serial.println("ESPUIclient:SendControlsToClient: Tell client we are starting a transfer of controls.");
    +            document["type"] = (ClientUpdateType_t::RebuildNeeded == TransferMode) ? UI_INITIAL_GUI : UI_EXTEND_GUI;
    +        }
    +        // Serial.println(String("ESPUIclient:SendControlsToClient:type: ") + String((uint32_t)document["type"]));
    +
    +        // Serial.println("ESPUIclient:SendControlsToClient: Build Controls.");
    +        if(prepareJSONChunk(startidx, document, ClientUpdateType_t::UpdateNeeded == TransferMode))
    +        {
    +            #if defined(DEBUG_ESPUI)
    +                if (ESPUI.verbosity >= Verbosity::VerboseJSON)
    +                {
    +                    Serial.println(F("ESPUIclient:SendControlsToClient: Sending elements --------->"));
    +                    String json;
    +                    serializeJson(document, json);
    +                    Serial.println(json);
    +                }
    +            #endif
    +
    +            // Serial.println("ESPUIclient:SendControlsToClient: Send message.");
    +            if(true == SendJsonDocToWebSocket(document))
    +            {
    +                // Serial.println("ESPUIclient:SendControlsToClient: Sent.");
    +            }
    +            else
    +            {
    +                // Serial.println("ESPUIclient:SendControlsToClient: Send failed.");
    +            }
    +        }
    +        else
    +        {
    +            // Serial.println("ESPUIclient:SendControlsToClient: No elements to send.");
    +            Response = true;
    +        }
    +
    +    } while(false);
    +
    +    // Serial.println(String("ESPUIclient:SendControlsToClient:Response: ") + String(Response));
    +    return Response;
    +}
    +
    +bool ESPUIclient::SendJsonDocToWebSocket(DynamicJsonDocument& document)
    +{
    +    bool Response = true;
    +
    +    do // once
    +    {
    +        if (!CanSend())
    +        {
    +            #if defined(DEBUG_ESPUI)
    +                if (ESPUI.verbosity >= Verbosity::VerboseJSON)
    +                {
    +                    Serial.println("SendJsonDocToWebSocket: Cannot Send to client. Not sending websocket message");
    +                }
    +            #endif
    +            // Serial.println("SendJsonDocToWebSocket: Cannot Send to client. Not sending websocket message");
    +            Response = false;
    +            break;
    +        }
    +
    +        String json;
    +        json.reserve(document.size() / 2);
    +        json.clear();
    +        serializeJson(document, json);
    +
    +        #if defined(DEBUG_ESPUI)
    +            if (ESPUI.verbosity >= Verbosity::VerboseJSON)
    +            {
    +                Serial.println(String("SendJsonDocToWebSocket: json: '") + json + "'");
    +            }
    +        #endif
    +
    +        #if defined(DEBUG_ESPUI)
    +            if (ESPUI.verbosity >= Verbosity::VerboseJSON)
    +            {
    +                Serial.println(F("SendJsonDocToWebSocket: client.text"));
    +            }
    +        #endif
    +        // Serial.println(F("SendJsonDocToWebSocket: client.text"));
    +        client->text(json);
    +
    +    } while (false);
    +
    +    return Response;
    +}
    +
    +void ESPUIclient::SetState(ClientUpdateType_t value)
    +{
    +    // only a higher priority state request can replace the current state request
    +    if(uint32_t(ClientUpdateType) < uint32_t(value))
    +    {
    +        ClientUpdateType = value;
    +    }
    +}
    +
    diff --git a/src/ESPUIclient.h b/src/ESPUIclient.h
    new file mode 100644
    index 0000000..5a34699
    --- /dev/null
    +++ b/src/ESPUIclient.h
    @@ -0,0 +1,62 @@
    +#pragma once
    +
    +#include 
    +#include 
    +#include 
    +#include "ESPUIclientFsm.h"
    +#include "ESPUIcontrol.h"
    +
    +class ESPUIclient
    +{
    +public:
    +    enum ClientUpdateType_t
    +    { // this is an orderd list. highest number is highest priority
    +        Synchronized    = 0,
    +        UpdateNeeded    = 1,
    +        RebuildNeeded   = 2,
    +        ReloadNeeded    = 3,
    +    };
    +
    +protected:
    +    // bool HasBeenNotified      = false;  // Set when a notification has been sent and we are waiting for a reply
    +    // bool DelayedNotification  = false;  // set if a delayed notification is needed
    +
    +    ClientUpdateType_t ClientUpdateType = ClientUpdateType_t::RebuildNeeded;
    +
    +    AsyncWebSocketClient * client = nullptr;
    +
    +    friend class fsm_EspuiClient_state_Idle;
    +    friend class fsm_EspuiClient_state_SendingUpdate;
    +    friend class fsm_EspuiClient_state_Rebuilding;
    +    friend class fsm_EspuiClient_state_WaitForAck;
    +    friend class fsm_EspuiClient_state_Reloading;
    +    friend class fsm_EspuiClient_state;
    +
    +    fsm_EspuiClient_state_Idle          fsm_EspuiClient_state_Idle_imp;
    +    fsm_EspuiClient_state_SendingUpdate fsm_EspuiClient_state_SendingUpdate_imp;
    +    fsm_EspuiClient_state_Rebuilding    fsm_EspuiClient_state_Rebuilding_imp;
    +    fsm_EspuiClient_state_Reloading     fsm_EspuiClient_state_Reloading_imp;
    +    fsm_EspuiClient_state* pCurrentFsmState = &fsm_EspuiClient_state_Idle_imp;
    +
    +    time_t      EspuiClientEndTime = 0;
    +
    +    // bool        NeedsNotification() { return pCurrentFsmState != &fsm_EspuiClient_state_Idle_imp; }
    +
    +    bool        CanSend();
    +    void        FillInHeader(ArduinoJson::DynamicJsonDocument& document);
    +    uint32_t    prepareJSONChunk(uint16_t startindex, DynamicJsonDocument& rootDoc, bool InUpdateMode);
    +    bool        SendControlsToClient(uint16_t startidx, ClientUpdateType_t TransferMode);
    +
    +    bool        SendClientNotification(ClientUpdateType_t value);
    +
    +public:
    +                ESPUIclient(AsyncWebSocketClient * _client);
    +                ESPUIclient(const ESPUIclient & source);
    +    virtual     ~ESPUIclient();
    +    void        NotifyClient(ClientUpdateType_t value);
    +    void        onWsEvent(AwsEventType type, void* arg, uint8_t* data, size_t len);
    +    bool        IsSyncronized();
    +    uint32_t    id() { return client->id(); }
    +    void        SetState(ClientUpdateType_t value);
    +    bool        SendJsonDocToWebSocket(ArduinoJson::DynamicJsonDocument& document);
    +};
    diff --git a/src/ESPUIclientFsm.cpp b/src/ESPUIclientFsm.cpp
    new file mode 100644
    index 0000000..1bd628b
    --- /dev/null
    +++ b/src/ESPUIclientFsm.cpp
    @@ -0,0 +1,107 @@
    +#include "ESPUI.h"
    +#include "ESPUIclient.h"
    +
    +//----------------------------------------------
    +// FSM definitions
    +//----------------------------------------------
    +void fsm_EspuiClient_state::Init()
    +{
    +    // Serial.println(String("fsm_EspuiClient_state:Init: ") + GetStateName());
    +    Parent->pCurrentFsmState = this;
    +}
    +
    +//----------------------------------------------
    +//----------------------------------------------
    +//----------------------------------------------
    +bool fsm_EspuiClient_state_Idle::NotifyClient()
    +{
    +    bool Response = false;
    +
    +    // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient"));
    +    ClientUpdateType_t TypeToProcess = Parent->ClientUpdateType;
    +    // Clear the type so that we capture any changes in type that happen
    +    // while we are processing the current request.
    +    Parent->ClientUpdateType = ClientUpdateType_t::Synchronized;
    +
    +    // Start processing the current request.
    +    switch (TypeToProcess)
    +    {
    +        case ClientUpdateType_t::Synchronized:
    +        {
    +            // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:Synchronized"));
    +            // Parent->fsm_EspuiClient_state_Idle_imp.Init();
    +            Response = true; // Parent->SendClientNotification(ClientUpdateType_t::UpdateNeeded);
    +            break;
    +        }
    +        case ClientUpdateType_t::UpdateNeeded:
    +        {
    +            // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:UpdateNeeded"));
    +            Parent->fsm_EspuiClient_state_SendingUpdate_imp.Init();
    +            Response = Parent->SendClientNotification(ClientUpdateType_t::UpdateNeeded);
    +            break;
    +        }
    +        case ClientUpdateType_t::RebuildNeeded:
    +        {
    +            // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:RebuildNeeded"));
    +            Parent->fsm_EspuiClient_state_Rebuilding_imp.Init();
    +            Response = Parent->SendClientNotification(ClientUpdateType_t::RebuildNeeded);
    +            break;
    +        }
    +        case ClientUpdateType_t::ReloadNeeded:
    +        {
    +            // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:ReloadNeeded"));
    +            Parent->fsm_EspuiClient_state_Reloading_imp.Init();
    +            Response = Parent->SendClientNotification(ClientUpdateType_t::ReloadNeeded);
    +            break;
    +        }
    +    }
    +    return Response;
    +}
    +
    +void fsm_EspuiClient_state_Idle::ProcessAck(uint16_t)
    +{
    +    // This is an unexpected request for control data from the browser
    +    // treat it as if it was a rebuild operation
    +    // Serial.println(F("fsm_EspuiClient_state_Idle: ProcessAck"));
    +    Parent->NotifyClient(ClientUpdateType_t::RebuildNeeded);
    +}
    +
    +//----------------------------------------------
    +//----------------------------------------------
    +//----------------------------------------------
    +bool fsm_EspuiClient_state_SendingUpdate::NotifyClient()
    +{
    +    // Serial.println(F("fsm_EspuiClient_state_SendingUpdate:NotifyClient"));
    +    return true; /* Ignore request */
    +}
    +
    +void fsm_EspuiClient_state_SendingUpdate::ProcessAck(uint16_t ControlIndex)
    +{
    +    // Serial.println(F("fsm_EspuiClient_state_SendingUpdate: ProcessAck"));
    +    if(Parent->SendControlsToClient(ControlIndex, ClientUpdateType_t::UpdateNeeded))
    +    {
    +        // No more data to send. Go back to idle or start next request
    +        Parent->fsm_EspuiClient_state_Idle_imp.Init();
    +        Parent->fsm_EspuiClient_state_Idle_imp.NotifyClient();
    +    }
    +}
    +
    +//----------------------------------------------
    +//----------------------------------------------
    +//----------------------------------------------
    +bool fsm_EspuiClient_state_Rebuilding::NotifyClient()
    +{
    +    // Serial.println(F("fsm_EspuiClient_state_Rebuilding: NotifyClient"));
    +    return true; /* Ignore request */
    +}
    +
    +void fsm_EspuiClient_state_Rebuilding::ProcessAck(uint16_t ControlIndex)
    +{
    +    // Serial.println(F("fsm_EspuiClient_state_Rebuilding: ProcessAck"));
    +    if(Parent->SendControlsToClient(ControlIndex, ClientUpdateType_t::RebuildNeeded))
    +    {
    +        // No more data to send. Go back to idle or start next request
    +        Parent->fsm_EspuiClient_state_Idle_imp.Init();
    +        Parent->fsm_EspuiClient_state_Idle_imp.NotifyClient();
    +    }
    +}
    diff --git a/src/ESPUIclientFsm.h b/src/ESPUIclientFsm.h
    new file mode 100644
    index 0000000..b04bb54
    --- /dev/null
    +++ b/src/ESPUIclientFsm.h
    @@ -0,0 +1,79 @@
    +#pragma once
    +
    +#include 
    +#include 
    +
    +// forward declaration
    +class ESPUIclient;
    +
    +/*****************************************************************************/
    +/*
    +*	Generic fsm base class.
    +*/
    +/*****************************************************************************/
    +/*****************************************************************************/
    +class fsm_EspuiClient_state
    +{
    +public:
    +                 fsm_EspuiClient_state() {};
    +    virtual     ~fsm_EspuiClient_state() {}
    +
    +            void Init();
    +    virtual bool NotifyClient() = 0;
    +    virtual void ProcessAck(uint16_t id) = 0;
    +    virtual String GetStateName () = 0;
    +            void SetParent(ESPUIclient * value) { Parent = value; }
    +
    +protected:
    +    ESPUIclient * Parent = nullptr;
    +
    +}; // fsm_EspuiClient_state
    +
    +class fsm_EspuiClient_state_Idle : public fsm_EspuiClient_state
    +{
    +public:
    +                 fsm_EspuiClient_state_Idle() {}
    +    virtual     ~fsm_EspuiClient_state_Idle() {}
    +
    +    virtual bool NotifyClient();
    +    virtual void ProcessAck(uint16_t id);
    +            String GetStateName() { return String(F("Idle")); }
    +
    +}; // fsm_EspuiClient_state_Idle
    +
    +class fsm_EspuiClient_state_SendingUpdate : public fsm_EspuiClient_state
    +{
    +public:
    +                 fsm_EspuiClient_state_SendingUpdate() {}
    +    virtual     ~fsm_EspuiClient_state_SendingUpdate() {}
    +
    +    virtual bool NotifyClient();
    +    virtual void ProcessAck(uint16_t id);
    +            String GetStateName() { return String(F("Sending Update")); }
    +
    +}; // fsm_EspuiClient_state_SendingUpdate
    +
    +class fsm_EspuiClient_state_Rebuilding : public fsm_EspuiClient_state
    +{
    +public:
    +                 fsm_EspuiClient_state_Rebuilding() {}
    +    virtual     ~fsm_EspuiClient_state_Rebuilding() {}
    +
    +    virtual bool NotifyClient();
    +    virtual void ProcessAck(uint16_t id);
    +            String GetStateName() { return String(F("Sending Rebuild")); }
    +
    +}; // fsm_EspuiClient_state_Rebuilding
    +
    +class fsm_EspuiClient_state_Reloading : public fsm_EspuiClient_state
    +{
    +public:
    +                 fsm_EspuiClient_state_Reloading() {}
    +    virtual     ~fsm_EspuiClient_state_Reloading() {}
    +
    +    virtual bool NotifyClient() { return false; }
    +    virtual void ProcessAck(uint16_t) {}
    +            String GetStateName() { return String(F("Reloading")); }
    +
    +}; // fsm_EspuiClient_state_Reloading
    +
    diff --git a/src/ESPUIcontrol.cpp b/src/ESPUIcontrol.cpp
    new file mode 100644
    index 0000000..e9c973c
    --- /dev/null
    +++ b/src/ESPUIcontrol.cpp
    @@ -0,0 +1,251 @@
    +#include "ESPUI.h"
    +
    +static uint16_t idCounter = 0;
    +static const String ControlError = "*** ESPUI ERROR: Could not transfer control ***";
    +
    +Control::Control(ControlType type, const char* label, void (*callback)(Control*, int, void*), void* UserData,
    +    const String& value, ControlColor color, bool visible, uint16_t parentControl)
    +    : type(type),
    +      label(label),
    +      callback(nullptr),
    +      extendedCallback(callback),
    +      user(UserData),
    +      value(value),
    +      color(color),
    +      visible(visible),
    +      wide(false),
    +      vertical(false),
    +      enabled(true),
    +      parentControl(parentControl),
    +      next(nullptr)
    +{
    +    id = ++idCounter;
    +}
    +
    +Control::Control(const Control& Control)
    +    : type(Control.type),
    +        id(Control.id),
    +        label(Control.label),
    +        callback(Control.callback),
    +        extendedCallback(Control.extendedCallback),
    +        user(Control.user),
    +        value(Control.value),
    +        color(Control.color),
    +        visible(Control.visible),
    +        parentControl(Control.parentControl),
    +        next(Control.next)
    +{ }
    +
    +void Control::SendCallback(int type)
    +{
    +    if(callback)
    +    {
    +        callback(this, type);
    +    }
    +
    +    if (extendedCallback)
    +    {
    +        extendedCallback(this, type, user);
    +    }
    +}
    +
    +void Control::DeleteControl() 
    +{
    +    ControlSyncState = ControlSyncState_t::deleted;
    +    extendedCallback = nullptr;
    +    callback = nullptr;
    +}
    +
    +void Control::MarshalControl(JsonObject & item, bool refresh)
    +{
    +    item[F("id")]      = id;
    +    if(refresh)
    +    {
    +        item[F("type")] = uint32_t(type) + uint32_t(ControlType::UpdateOffset);
    +    }
    +    else
    +    {
    +        item[F("type")] = uint32_t(type);
    +    }
    +    item[F("label")]   = label;
    +    item[F("value")]   = value;
    +    item[F("visible")] = visible;
    +    item[F("color")]   = (int)color;
    +    item[F("enabled")] = enabled;
    +
    +    if (!panelStyle.isEmpty())    {item[F("panelStyle")]    = panelStyle;}
    +    if (!elementStyle.isEmpty())  {item[F("elementStyle")]  = elementStyle;}
    +    if (!inputType.isEmpty())     {item[F("inputType")]     = inputType;}
    +    if (wide == true)             {item[F("wide")]          = true;}
    +    if (vertical == true)         {item[F("vertical")]      = true;}
    +    if (parentControl != Control::noParent)
    +    {
    +        item[F("parentControl")] = String(parentControl);
    +    }
    +
    +    // special case for selects: to preselect an option, you have to add
    +    // "selected" to