diff --git a/data/js/controls.js b/data/js/controls.js index 7a436c0..d1dc71b 100644 --- a/data/js/controls.js +++ b/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,726 +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; - - 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) { - //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" + 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" ); - element.addClass("slider-" + colorClass(data.color)); - } else { - element.removeClass( - "turquoise emerald peterriver wetasphalt sunflower carrot alizarin" - ); - element.addClass(colorClass(data.color)); - } - - processEnabled(data); + } else { + websock = new WebSocket("ws://" + window.location.hostname + "/ws"); } - $(".range-slider__range").each(function(){ - $(this)[0].value = $(this).attr("value"); - $(this).next().html($(this).attr("value")); - }); - }; + // 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.onmessage = handleEvent; + 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); + // start the update over again + websock.send("uiok:" + 0); + return; + } + 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; + } + + 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")); + }); + }; + + 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, "")); - }; + 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.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"); - } - }); + 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" : ""; +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"); + 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 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); + + } 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)); } - - 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)); - } } -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 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;
    +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_NUMBER:
    -    case UPDATE_NUMBER:
    -      $("#num" + data.id).prop("disabled", !data.enabled);
    -      break;
    +        case UI_SLIDER:
    +        case UPDATE_SLIDER:
    +            $("#sl" + 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_NUMBER:
    +        case UPDATE_NUMBER:
    +            $("#num" + data.id).prop("disabled", !data.enabled);
    +            break;
     
    -    case UI_BUTTON:
    -    case UPDATE_BUTTON:
    -      $("#btn" + 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_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;
    -  }
    +        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/data/js/controls.min.js b/data/js/controls.min.js
    index 1e7dd13..b387a49 100644
    --- a/data/js/controls.min.js
    +++ b/data/js/controls.min.js
    @@ -1,104 +1,107 @@
    -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;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("");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");}
    +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);},});}
    +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
    diff --git a/data/js/graph.min.js b/data/js/graph.min.js
    index b113ea1..7d11b2d 100644
    --- a/data/js/graph.min.js
    +++ b/data/js/graph.min.js
    @@ -1,15 +1,15 @@
    -function lineGraph(parent,xAccessor,yAccessor){const width=620;const height=420;const gutter=40;const pixelsPerTick=30;function numericTransformer(dataMin,dataMax,pxMin,pxMax){var dataDiff=dataMax-dataMin,pxDiff=pxMax-pxMin,dataRatio=pxDiff/dataDiff,coordRatio=dataDiff/pxDiff;return{toCoord:function(data){return(data-dataMin)*dataRatio+pxMin;},toData:function(coord){return(coord-pxMin)*coordRatio+dataMin;}};}
    -function axisRenderer(orientation,transform){var axisGroup=document.createElementNS("http://www.w3.org/2000/svg","g");var axisPath=document.createElementNS("http://www.w3.org/2000/svg","path");axisGroup.setAttribute("class",orientation+"-axis");var xMin=gutter;var xMax=width-gutter;var yMin=height-gutter;var yMax=gutter;if(orientation==="x"){axisPath.setAttribute("d","M "+xMin+" "+yMin+" L "+xMax+" "+yMin);for(var i=xMin;i<=xMax;i++){if((i-xMin)%pixelsPerTick===0&&i!==xMin){var text=document.createElementNS("http://www.w3.org/2000/svg","text");text.innerHTML=Math.floor(transform(i));text.setAttribute("x",i);text.setAttribute("y",yMin);text.setAttribute("dy","1em");axisGroup.appendChild(text);}}}else{axisPath.setAttribute("d","M "+xMin+" "+yMin+" L "+xMin+" "+yMax);for(var i=yMax;i<=yMin;i++){if((i-yMin)%pixelsPerTick===0&&i!==yMin){var tickGroup=document.createElementNS("http://www.w3.org/2000/svg","g");var gridLine=document.createElementNS("http://www.w3.org/2000/svg","path");text=document.createElementNS("http://www.w3.org/2000/svg","text");text.innerHTML=Math.floor(transform(i));text.setAttribute("x",xMin);text.setAttribute("y",i);text.setAttribute("dx","-.5em");text.setAttribute("dy",".3em");gridLine.setAttribute("d","M "+xMin+" "+i+" L "+xMax+" "+i);tickGroup.appendChild(gridLine);tickGroup.appendChild(text);axisGroup.appendChild(tickGroup);}}}
    -axisGroup.appendChild(axisPath);parent.appendChild(axisGroup);}
    -function lineRenderer(xAccessor,yAccessor,xTransform,yTransform){var line=document.createElementNS("http://www.w3.org/2000/svg","path");xAccessor.reset();yAccessor.reset();if(!xAccessor.hasNext()||!yAccessor.hasNext()){return;}
    -var pathString="M "+xTransform(xAccessor.next())+" "+yTransform(yAccessor.next());while(xAccessor.hasNext()&&yAccessor.hasNext()){pathString+=" L "+
    -xTransform(xAccessor.next())+
    -" "+
    -yTransform(yAccessor.next());}
    -line.setAttribute("class","series");line.setAttribute("d",pathString);parent.appendChild(line);}
    -function pointRenderer(xAccessor,yAccessor,xTransform,yTransform){var pointGroup=document.createElementNS("http://www.w3.org/2000/svg","g");pointGroup.setAttribute("class","data-points");xAccessor.reset();yAccessor.reset();if(!xAccessor.hasNext()||!yAccessor.hasNext()){return;}
    -while(xAccessor.hasNext()&&yAccessor.hasNext()){var xDataValue=xAccessor.next();var x=xTransform(xDataValue);var yDataValue=yAccessor.next();var y=yTransform(yDataValue);var circle=document.createElementNS("http://www.w3.org/2000/svg","circle");circle.setAttribute("cx",x);circle.setAttribute("cy",y);circle.setAttribute("r","4");var text=document.createElementNS("http://www.w3.org/2000/svg","text");text.innerHTML=Math.floor(xDataValue)+" / "+Math.floor(yDataValue);text.setAttribute("x",x);text.setAttribute("y",y);text.setAttribute("dx","1em");text.setAttribute("dy","-.7em");pointGroup.appendChild(circle);pointGroup.appendChild(text);}
    -parent.appendChild(pointGroup);}
    -xTransform=numericTransformer(xAccessor.min(),xAccessor.max(),0+gutter,width-gutter);yTransform=numericTransformer(yAccessor.min(),yAccessor.max(),height-gutter,0+gutter);axisRenderer("x",xTransform.toData);axisRenderer("y",yTransform.toData);lineRenderer(xAccessor,yAccessor,xTransform.toCoord,yTransform.toCoord);pointRenderer(xAccessor,yAccessor,xTransform.toCoord,yTransform.toCoord);}
    -function renderGraphSvg(dataArray,renderId){var figure=document.getElementById(renderId);while(figure.hasChildNodes()){figure.removeChild(figure.lastChild);}
    +function lineGraph(parent,xAccessor,yAccessor){const width=620;const height=420;const gutter=40;const pixelsPerTick=30;function numericTransformer(dataMin,dataMax,pxMin,pxMax){var dataDiff=dataMax-dataMin,pxDiff=pxMax-pxMin,dataRatio=pxDiff/dataDiff,coordRatio=dataDiff/pxDiff;return{toCoord:function(data){return(data-dataMin)*dataRatio+pxMin;},toData:function(coord){return(coord-pxMin)*coordRatio+dataMin;}};}
    +function axisRenderer(orientation,transform){var axisGroup=document.createElementNS("http://www.w3.org/2000/svg","g");var axisPath=document.createElementNS("http://www.w3.org/2000/svg","path");axisGroup.setAttribute("class",orientation+"-axis");var xMin=gutter;var xMax=width-gutter;var yMin=height-gutter;var yMax=gutter;if(orientation==="x"){axisPath.setAttribute("d","M "+xMin+" "+yMin+" L "+xMax+" "+yMin);for(var i=xMin;i<=xMax;i++){if((i-xMin)%pixelsPerTick===0&&i!==xMin){var text=document.createElementNS("http://www.w3.org/2000/svg","text");text.innerHTML=Math.floor(transform(i));text.setAttribute("x",i);text.setAttribute("y",yMin);text.setAttribute("dy","1em");axisGroup.appendChild(text);}}}else{axisPath.setAttribute("d","M "+xMin+" "+yMin+" L "+xMin+" "+yMax);for(var i=yMax;i<=yMin;i++){if((i-yMin)%pixelsPerTick===0&&i!==yMin){var tickGroup=document.createElementNS("http://www.w3.org/2000/svg","g");var gridLine=document.createElementNS("http://www.w3.org/2000/svg","path");text=document.createElementNS("http://www.w3.org/2000/svg","text");text.innerHTML=Math.floor(transform(i));text.setAttribute("x",xMin);text.setAttribute("y",i);text.setAttribute("dx","-.5em");text.setAttribute("dy",".3em");gridLine.setAttribute("d","M "+xMin+" "+i+" L "+xMax+" "+i);tickGroup.appendChild(gridLine);tickGroup.appendChild(text);axisGroup.appendChild(tickGroup);}}}
    +axisGroup.appendChild(axisPath);parent.appendChild(axisGroup);}
    +function lineRenderer(xAccessor,yAccessor,xTransform,yTransform){var line=document.createElementNS("http://www.w3.org/2000/svg","path");xAccessor.reset();yAccessor.reset();if(!xAccessor.hasNext()||!yAccessor.hasNext()){return;}
    +var pathString="M "+xTransform(xAccessor.next())+" "+yTransform(yAccessor.next());while(xAccessor.hasNext()&&yAccessor.hasNext()){pathString+=" L "+
    +xTransform(xAccessor.next())+
    +" "+
    +yTransform(yAccessor.next());}
    +line.setAttribute("class","series");line.setAttribute("d",pathString);parent.appendChild(line);}
    +function pointRenderer(xAccessor,yAccessor,xTransform,yTransform){var pointGroup=document.createElementNS("http://www.w3.org/2000/svg","g");pointGroup.setAttribute("class","data-points");xAccessor.reset();yAccessor.reset();if(!xAccessor.hasNext()||!yAccessor.hasNext()){return;}
    +while(xAccessor.hasNext()&&yAccessor.hasNext()){var xDataValue=xAccessor.next();var x=xTransform(xDataValue);var yDataValue=yAccessor.next();var y=yTransform(yDataValue);var circle=document.createElementNS("http://www.w3.org/2000/svg","circle");circle.setAttribute("cx",x);circle.setAttribute("cy",y);circle.setAttribute("r","4");var text=document.createElementNS("http://www.w3.org/2000/svg","text");text.innerHTML=Math.floor(xDataValue)+" / "+Math.floor(yDataValue);text.setAttribute("x",x);text.setAttribute("y",y);text.setAttribute("dx","1em");text.setAttribute("dy","-.7em");pointGroup.appendChild(circle);pointGroup.appendChild(text);}
    +parent.appendChild(pointGroup);}
    +xTransform=numericTransformer(xAccessor.min(),xAccessor.max(),0+gutter,width-gutter);yTransform=numericTransformer(yAccessor.min(),yAccessor.max(),height-gutter,0+gutter);axisRenderer("x",xTransform.toData);axisRenderer("y",yTransform.toData);lineRenderer(xAccessor,yAccessor,xTransform.toCoord,yTransform.toCoord);pointRenderer(xAccessor,yAccessor,xTransform.toCoord,yTransform.toCoord);}
    +function renderGraphSvg(dataArray,renderId){var figure=document.getElementById(renderId);while(figure.hasChildNodes()){figure.removeChild(figure.lastChild);}
     var svg=document.createElementNS("http://www.w3.org/2000/svg","svg");svg.setAttribute("viewBox","0 0 640 440");svg.setAttribute("preserveAspectRatio","xMidYMid meet");lineGraph(svg,(function(data,min,max){var i=0;return{hasNext:function(){return i
    '+ -'
    0
    '+ -"
    ";return tmplt;} +function rkmd_rangeSlider(selector){var self,slider_width,slider_offset,curnt,sliderDiscrete,range,slider;self=$(selector);slider_width=self.width();slider_offset=self.offset().left;sliderDiscrete=self;sliderDiscrete.each(function(i,v){curnt=$(this);curnt.append(sliderDiscrete_tmplt());range=curnt.find('input[type="range"]');slider=curnt.find(".slider");slider_fill=slider.find(".slider-fill");slider_handle=slider.find(".slider-handle");slider_label=slider.find(".slider-label");var range_val=parseInt(range.val());slider_fill.css("width",range_val+"%");slider_handle.css("left",range_val+"%");slider_label.find("span").text(range_val);});self.on("mousedown touchstart",".slider-handle",function(e){if(e.button===2){return false;} +var parents=$(this).parents(".rkmd-slider");var slider_width=parents.width();var slider_offset=parents.offset().left;var check_range=parents.find('input[type="range"]').is(":disabled");if(check_range===true){return false;} +$(this).addClass("is-active");var moveFu=function(e){var pageX=e.pageX||e.changedTouches[0].pageX;var slider_new_width=pageX-slider_offset;if(slider_new_width<=slider_width&&!(slider_new_width<"0")){slider_move(parents,slider_new_width,slider_width,true);}};var upFu=function(e){$(this).off(handlers);parents.find(".is-active").removeClass("is-active");};var handlers={mousemove:moveFu,touchmove:moveFu,mouseup:upFu,touchend:upFu,};$(document).on(handlers);});self.on("mousedown touchstart",".slider",function(e){if(e.button===2){return false;} +var parents=$(this).parents(".rkmd-slider");var slider_width=parents.width();var slider_offset=parents.offset().left;var check_range=parents.find('input[type="range"]').is(":disabled");if(check_range===true){return false;} +var slider_new_width=e.pageX-slider_offset;if(slider_new_width<=slider_width&&!(slider_new_width<"0")){slider_move(parents,slider_new_width,slider_width,true);} +var upFu=function(e){$(this).off(handlers);};var handlers={mouseup:upFu,touchend:upFu,};$(document).on(handlers);});} +function sliderDiscrete_tmplt(){var tmplt='
    '+ +'
    '+ +'
    0
    '+ +"
    ";return tmplt;} function slider_move(parents,newW,sliderW,send){var slider_new_val=parseInt(Math.round((newW/sliderW)*100));var slider_fill=parents.find(".slider-fill");var slider_handle=parents.find(".slider-handle");var range=parents.find('input[type="range"]');range.next().html(newW);slider_fill.css("width",slider_new_val+"%");slider_handle.css({left:slider_new_val+"%",transition:"none","-webkit-transition":"none","-moz-transition":"none",});range.val(slider_new_val);if(parents.find(".slider-handle span").text()!=slider_new_val){parents.find(".slider-handle span").text(slider_new_val);var number=parents.attr("id").substring(2);if(send)websock.send("slvalue:"+slider_new_val+":"+number);}} \ No newline at end of file diff --git a/data/js/tabbedcontent.min.js b/data/js/tabbedcontent.min.js index efbf454..e6925c0 100644 --- a/data/js/tabbedcontent.min.js +++ b/data/js/tabbedcontent.min.js @@ -1,35 +1,35 @@ -;(function($,document,window,undefined){"use strict";var Tabbedcontent=function(tabcontent,options){var defaults={links:tabcontent.prev().find('a').length?tabcontent.prev().find('a'):'.tabs a',errorSelector:'.error-message',speed:false,onSwitch:false,onInit:false,currentClass:'active',tabErrorClass:'has-errors',history:true,historyOnInit:true,loop:false},firstTime=false,children=tabcontent.children(),history=window.history,loc=document.location,current=null;options=$.extend(defaults,options);if(!(options.links instanceof $)){options.links=$(options.links);} -function tabExists(tab){return Boolean(children.filter(tab).length);} -function isFirst(){return current===0;} -function isInt(num){return num%1===0;} -function isLast(){return current===children.length-1;} -function filterTab(tab){return $(this).attr('href').match(new RegExp(tab+'$'));} -function getTab(tab){if(tab instanceof $){return{tab:tab,link:options.links.eq(tab.index())};} -if(isInt(tab)){return{tab:children.eq(tab),link:options.links.eq(tab)};} -if(children.filter(tab).length){return{tab:children.filter(tab),link:options.links.filter(function(){return filterTab.apply(this,[tab]);})};} -return{tab:children.filter('#'+tab),link:options.links.filter(function(){return filterTab.apply(this,['#'+tab]);})};} -function getCurrent(){return options.links.parent().filter('.'+options.currentClass).index();} -function next(loop){++current;if(loop===undefined)loop=options.loop;if(current=children.length){return switchTab(0,true);} -return false;} -function prev(loop){--current;if(loop===undefined)loop=options.loop;if(current>=0){return switchTab(current,true);}else if(loop&¤t<0){return switchTab(children.length-1,true);} -return false;} -function onSwitch(tab){if(options.history&&options.historyOnInit&&firstTime&&history!==undefined&&('pushState'in history)){firstTime=false;window.setTimeout(function(){history.replaceState(null,'',tab);},100);} -current=getCurrent();if(options.onSwitch&&typeof options.onSwitch==='function'){options.onSwitch(tab,api());} -tabcontent.trigger('tabcontent.switch',[tab,api()]);} -function switchTab(tab,api){if(!tab.toString().match(/^#/)){tab='#'+getTab(tab).tab.attr('id');} -if(!tabExists(tab)){return false;} -options.links.attr('aria-selected','false').parent().removeClass(options.currentClass);options.links.filter(function(){return filterTab.apply(this,[tab]);}).attr('aria-selected','true').parent().addClass(options.currentClass);children.hide();if(options.history&&api){if(history!==undefined&&('pushState'in history)){history.pushState(null,'',tab);}else{window.location.hash=tab;}} -children.attr('aria-hidden','true').filter(tab).show(options.speed,function(){if(options.speed){onSwitch(tab);}}).attr('aria-hidden','false');if(!options.speed){onSwitch(tab);} -return true;} -function apiSwitch(tab){return switchTab(tab,true);} -function hashSwitch(e){switchTab(loc.hash);} -function init(){if(tabExists(loc.hash)){switchTab(loc.hash);} -else if(options.links.parent().filter('.'+options.currentClass).length){switchTab(options.links.parent().filter('.'+options.currentClass).index());} -else if(options.errorSelector&&children.find(options.errorSelector).length){children.each(function(){if($(this).find(options.errorSelector).length){switchTab("#"+$(this).attr("id"));return false;}});} -else{switchTab("#"+children.filter(":first-child").attr("id"));} -if(options.errorSelector){children.find(options.errorSelector).each(function(){var tab=getTab($(this).parent());tab.link.parent().addClass(options.tabErrorClass);});} -if('onhashchange'in window){$(window).bind('hashchange',hashSwitch);}else{var current_href=loc.href;window.setInterval(function(){if(current_href!==loc.href){hashSwitch.call(window.event);current_href=loc.href;}},100);} -$(options.links).on('click',function(e){switchTab($(this).attr('href').replace(/^[^#]+/,''),options.history);e.preventDefault();});if(options.onInit&&typeof options.onInit==='function'){options.onInit(api());} -tabcontent.trigger('tabcontent.init',[api()]);} -function api(){return{'switch':apiSwitch,'switchTab':apiSwitch,'getCurrent':getCurrent,'getTab':getTab,'next':next,'prev':prev,'isFirst':isFirst,'isLast':isLast};} +;(function($,document,window,undefined){"use strict";var Tabbedcontent=function(tabcontent,options){var defaults={links:tabcontent.prev().find('a').length?tabcontent.prev().find('a'):'.tabs a',errorSelector:'.error-message',speed:false,onSwitch:false,onInit:false,currentClass:'active',tabErrorClass:'has-errors',history:true,historyOnInit:true,loop:false},firstTime=false,children=tabcontent.children(),history=window.history,loc=document.location,current=null;options=$.extend(defaults,options);if(!(options.links instanceof $)){options.links=$(options.links);} +function tabExists(tab){return Boolean(children.filter(tab).length);} +function isFirst(){return current===0;} +function isInt(num){return num%1===0;} +function isLast(){return current===children.length-1;} +function filterTab(tab){return $(this).attr('href').match(new RegExp(tab+'$'));} +function getTab(tab){if(tab instanceof $){return{tab:tab,link:options.links.eq(tab.index())};} +if(isInt(tab)){return{tab:children.eq(tab),link:options.links.eq(tab)};} +if(children.filter(tab).length){return{tab:children.filter(tab),link:options.links.filter(function(){return filterTab.apply(this,[tab]);})};} +return{tab:children.filter('#'+tab),link:options.links.filter(function(){return filterTab.apply(this,['#'+tab]);})};} +function getCurrent(){return options.links.parent().filter('.'+options.currentClass).index();} +function next(loop){++current;if(loop===undefined)loop=options.loop;if(current=children.length){return switchTab(0,true);} +return false;} +function prev(loop){--current;if(loop===undefined)loop=options.loop;if(current>=0){return switchTab(current,true);}else if(loop&¤t<0){return switchTab(children.length-1,true);} +return false;} +function onSwitch(tab){if(options.history&&options.historyOnInit&&firstTime&&history!==undefined&&('pushState'in history)){firstTime=false;window.setTimeout(function(){history.replaceState(null,'',tab);},100);} +current=getCurrent();if(options.onSwitch&&typeof options.onSwitch==='function'){options.onSwitch(tab,api());} +tabcontent.trigger('tabcontent.switch',[tab,api()]);} +function switchTab(tab,api){if(!tab.toString().match(/^#/)){tab='#'+getTab(tab).tab.attr('id');} +if(!tabExists(tab)){return false;} +options.links.attr('aria-selected','false').parent().removeClass(options.currentClass);options.links.filter(function(){return filterTab.apply(this,[tab]);}).attr('aria-selected','true').parent().addClass(options.currentClass);children.hide();if(options.history&&api){if(history!==undefined&&('pushState'in history)){history.pushState(null,'',tab);}else{window.location.hash=tab;}} +children.attr('aria-hidden','true').filter(tab).show(options.speed,function(){if(options.speed){onSwitch(tab);}}).attr('aria-hidden','false');if(!options.speed){onSwitch(tab);} +return true;} +function apiSwitch(tab){return switchTab(tab,true);} +function hashSwitch(e){switchTab(loc.hash);} +function init(){if(tabExists(loc.hash)){switchTab(loc.hash);} +else if(options.links.parent().filter('.'+options.currentClass).length){switchTab(options.links.parent().filter('.'+options.currentClass).index());} +else if(options.errorSelector&&children.find(options.errorSelector).length){children.each(function(){if($(this).find(options.errorSelector).length){switchTab("#"+$(this).attr("id"));return false;}});} +else{switchTab("#"+children.filter(":first-child").attr("id"));} +if(options.errorSelector){children.find(options.errorSelector).each(function(){var tab=getTab($(this).parent());tab.link.parent().addClass(options.tabErrorClass);});} +if('onhashchange'in window){$(window).bind('hashchange',hashSwitch);}else{var current_href=loc.href;window.setInterval(function(){if(current_href!==loc.href){hashSwitch.call(window.event);current_href=loc.href;}},100);} +$(options.links).on('click',function(e){switchTab($(this).attr('href').replace(/^[^#]+/,''),options.history);e.preventDefault();});if(options.onInit&&typeof options.onInit==='function'){options.onInit(api());} +tabcontent.trigger('tabcontent.init',[api()]);} +function api(){return{'switch':apiSwitch,'switchTab':apiSwitch,'getCurrent':getCurrent,'getTab':getTab,'next':next,'prev':prev,'isFirst':isFirst,'isLast':isLast};} init();return api();};$.fn.tabbedContent=function(options){return this.each(function(){var tabs=new Tabbedcontent($(this),options);$(this).data('api',tabs);});};})(window.jQuery||window.Zepto||window.$,document,window); \ No newline at end of file diff --git a/pio_examples/gui/platformio.ini b/pio_examples/gui/platformio.ini index e366dd8..0f5497c 100644 --- a/pio_examples/gui/platformio.ini +++ b/pio_examples/gui/platformio.ini @@ -13,25 +13,31 @@ src_dir = ./src data_dir = ../../data [env] -lib_extra_dirs = ../../ +framework = arduino board_build.filesystem = littlefs +lib_extra_dirs = ../../ +lib_deps = + bblanchon/ArduinoJson @ ^6.18.5 + https://github.com/esphome/ESPAsyncWebServer @ 3.0.0 ; Updated lib, seems to have recent patches. + +lib_ignore = + ESP Async WebServer ; force the use of the esphome version + AsyncTCP ; force the use of the esphome version + LittleFS_esp32 ; force the use of the ESP32 built into the core version + ; Additional scripts: Usage: see https://github.com/s00500/ESPUI/issues/144#issuecomment-1005135077 ;extra_scripts = ; LittleFSBuilder.py [env:esp8266] platform = espressif8266 -framework = arduino board = nodemcuv2 -lib_deps = - bblanchon/ArduinoJson @ ^6.18.5 - me-no-dev/ESP Async WebServer @ ^1.2.3 [env:esp32] platform = espressif32 -framework = arduino board = esp32dev +monitor_filters = esp32_exception_decoder +board_build.flash_mode = dout lib_deps = - lorol/LittleFS_esp32@^1.0.6 - bblanchon/ArduinoJson @ ^6.18.5 - me-no-dev/ESP Async WebServer @ ^1.2.3 + ${env.lib_deps} + me-no-dev/AsyncTCP@1.1.1 diff --git a/src/ESPUI.cpp b/src/ESPUI.cpp index c6f3577..b65998c 100644 --- a/src/ESPUI.cpp +++ b/src/ESPUI.cpp @@ -13,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) @@ -26,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) { @@ -114,25 +108,17 @@ 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) - Serial.println(LITTLEFS.totalBytes()); - Serial.println(LITTLEFS.usedBytes()); + Serial.println(LittleFS.totalBytes()); + Serial.println(LittleFS.usedBytes()); #else FSInfo fs_info; @@ -146,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) @@ -171,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) @@ -205,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) @@ -276,21 +248,21 @@ 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...")); } #endif #if defined(ESP32) - LITTLEFS.format(); + LittleFS.format(); - if (!LITTLEFS.begin(true)) + if (!LittleFS.begin(true)) { #if defined(DEBUG_ESPUI) - if (this->verbosity) + if (verbosity) { - Serial.println(F("LITTLEFS Mount Failed")); + Serial.println(F("LittleFS Mount Failed")); } #endif @@ -298,10 +270,10 @@ void ESPUIClass::prepareFileSystem() } #if defined(DEBUG_ESPUI) - if (this->verbosity) + if (verbosity) { listDir("/", 1); - Serial.println(F("LITTLEFS Mount ESP32 Done")); + Serial.println(F("LittleFS Mount ESP32 Done")); } #endif @@ -310,7 +282,7 @@ void ESPUIClass::prepareFileSystem() LittleFS.begin(); #if defined(DEBUG_ESPUI) - if (this->verbosity) + if (verbosity) { Serial.println(F("LITTLEFS Mount ESP8266 Done")); } @@ -330,7 +302,7 @@ void ESPUIClass::prepareFileSystem() deleteFile("/js/tabbedcontent.js"); #if defined(DEBUG_ESPUI) - if (this->verbosity) + if (verbosity) { Serial.println(F("Cleanup done")); } @@ -350,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 :-)")); } @@ -359,7 +331,7 @@ void ESPUIClass::prepareFileSystem() #if defined(ESP32) #if defined(DEBUG_ESPUI) - if (this->verbosity) + if (verbosity) { listDir("/", 1); } @@ -367,237 +339,44 @@ 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) + // Serial.println(String("ESPUIClass::OnWsEvent: type: ") + String(type)); + RemoveToBeDeletedControls(); + + if(WS_EVT_DISCONNECT == type) { - case WS_EVT_DISCONNECT: { -#if defined(DEBUG_ESPUI) - if (ESPUI.verbosity) + #if defined(DEBUG_ESPUI) + if (verbosity) + { + Serial.println(F("WS_EVT_DISCONNECT")); + } + #endif + + if(MapOfClients.end() != MapOfClients.find(client->id())) { - 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 - - break; - } - - case WS_EVT_ERROR: { -#if defined(DEBUG_ESPUI) - if (ESPUI.verbosity) - { - Serial.print(F("WebSocket Error!\n")); - } -#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) - { - Serial.println(F("JSON Data Sent to Client!")); - } -#endif - } - 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(); - - #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 - - Control* c = ESPUI.getControl(id); - - if (c == nullptr) - { - #if defined(DEBUG_ESPUI) - if (ESPUI.verbosity) - { - Serial.print(F("No control found for ID ")); - Serial.println(id); - } - #endif - - return; - } - - if (false == c->HasCallback()) - { -#if defined(DEBUG_ESPUI) - if (ESPUI.verbosity) - { - Serial.print(F("No callback found for ID ")); - Serial.println(id); - } - #endif - - return; - } - - if (msg.startsWith(F("bdown:"))) - { - c->SendCallback(B_DOWN); - } - else if (msg.startsWith(F("bup:"))) - { - c->SendCallback(B_UP); - } - else if (msg.startsWith(F("pfdown:"))) - { - c->SendCallback(P_FOR_DOWN); - } - else if (msg.startsWith(F("pfup:"))) - { - c->SendCallback(P_FOR_UP); - } - else if (msg.startsWith(F("pldown:"))) - { - c->SendCallback(P_LEFT_DOWN); - } - else if (msg.startsWith(F("plup:"))) - { - c->SendCallback(P_LEFT_UP); - } - else if (msg.startsWith(F("prdown:"))) - { - c->SendCallback(P_RIGHT_DOWN); - } - else if (msg.startsWith(F("prup:"))) - { - c->SendCallback(P_RIGHT_UP); - } - else if (msg.startsWith(F("pbdown:"))) - { - c->SendCallback(P_BACK_DOWN); - } - else if (msg.startsWith(F("pbup:"))) - { - c->SendCallback(P_BACK_UP); - } - else if (msg.startsWith(F("pcdown:"))) - { - c->SendCallback(P_CENTER_DOWN); - } - else if (msg.startsWith(F("pcup:"))) - { - c->SendCallback(P_CENTER_UP); - } - else if (msg.startsWith(F("sactive:"))) - { - c->value = "1"; - ESPUI.updateControl(c, client->id()); - c->SendCallback(S_ACTIVE); - } - else if (msg.startsWith(F("sinactive:"))) - { - c->value = "0"; - ESPUI.updateControl(c, client->id()); - c->SendCallback(S_INACTIVE); - } - else if (msg.startsWith(F("slvalue:"))) - { - c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); - ESPUI.updateControl(c, client->id()); - c->SendCallback(SL_VALUE); - } - else if (msg.startsWith(F("nvalue:"))) - { - c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); - ESPUI.updateControl(c, client->id()); - c->SendCallback(N_VALUE); - } - else if (msg.startsWith(F("tvalue:"))) - { - c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); - ESPUI.updateControl(c, client->id()); - c->SendCallback(T_VALUE); - } - else if (msg.startsWith("tabvalue:")) - { - c->SendCallback(client->id()); - } - else if (msg.startsWith(F("svalue:"))) - { - c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); - ESPUI.updateControl(c, client->id()); - c->SendCallback(S_VALUE); - } - else if (msg.startsWith(F("time:"))) - { - c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); - ESPUI.updateControl(c, client->id()); - c->SendCallback(TM_VALUE); - } - else - { - #if defined(DEBUG_ESPUI) - if (ESPUI.verbosity) - { - Serial.println(F("Malformated message from the websocket")); - } - #endif - } + // Serial.println("Delete client."); + delete MapOfClients[client->id()]; + MapOfClients.erase(client->id()); } } - break; - - default: - break; + else + { + if(MapOfClients.end() == MapOfClients.find(client->id())) + { + // Serial.println("ESPUIClass::OnWsEvent:Create new client."); + MapOfClients[client->id()] = new ESPUIclient(client); + } + MapOfClients[client->id()]->onWsEvent(type, arg, data, len); } + + ClearControlUpdateFlags(); + + return; } uint16_t ESPUIClass::addControl(ControlType type, const char* label) @@ -630,15 +409,19 @@ uint16_t ESPUIClass::addControl(ControlType type, const char* label, const Strin uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color, uint16_t parentControl, void (*callback)(Control*, int, void *), void * UserData) { +#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) { @@ -648,50 +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) { - Control* PreviousControl = nullptr; - Control* CurrentControl = this->controls; + bool Response = false; - while(nullptr != CurrentControl) + Control* control = getControl(id); + if (control) { - if (id == CurrentControl->id) - { - break; - } - PreviousControl = CurrentControl; - CurrentControl = CurrentControl->next; - } + Response = true; + control->DeleteControl(); + controlCount--; - if (nullptr != CurrentControl) - { - if(nullptr == PreviousControl) - { - this->controls = CurrentControl->next; - } - else - { - PreviousControl->next = CurrentControl->next; - } - delete CurrentControl; - this->controlCount--; - if (force_reload_ui) + if(force_rebuild_ui) { jsonReload(); } else { - jsonDom(0); // resends to all + NotifyClients(ClientUpdateType_t::RebuildNeeded); } - return true; } +#ifdef DEBUG_ESPUI + else + { + // Serial.println(String("Could not Remove Control ") + String(id)); + } +#endif // def DEBUG_ESPUI - return false; + return Response; +} + +void ESPUIClass::RemoveToBeDeletedControls() +{ + #ifdef ESP32 + xSemaphoreTake(ControlsSemaphore, portMAX_DELAY); + #endif // def ESP32 + + Control* PreviousControl = nullptr; + Control* CurrentControl = controls; + + while (nullptr != CurrentControl) + { + Control* NextControl = CurrentControl->next; + if (CurrentControl->ToBeDeleted()) + { + if (CurrentControl == controls) + { + // this is the root control + controls = NextControl; + } + else + { + PreviousControl->next = NextControl; + } + delete CurrentControl; + CurrentControl = NextControl; + } + else + { + PreviousControl = CurrentControl; + CurrentControl = NextControl; + } + } + #ifdef ESP32 + xSemaphoreGive(ControlsSemaphore); + #endif // def ESP32 } uint16_t ESPUIClass::label(const char* label, ControlColor color, const String& value) @@ -810,87 +625,49 @@ uint16_t ESPUIClass::text(const char* label, void (*callback)(Control*, int, voi 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 +} - while (control != nullptr) +// 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 (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; - if (control->inputType.length()) - root["inputType"] = control->inputType; - 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; 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) @@ -936,6 +713,7 @@ 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); } @@ -956,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; @@ -985,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; @@ -996,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); } } @@ -1049,7 +848,7 @@ void ESPUIClass::updateGauge(uint16_t id, int number, int clientId) updateControlValue(id, String(number), clientId); } -void ESPUIClass::updateTime(uint16_t id, int clientId) +void ESPUIClass::updateTime(uint16_t id, int clientId) { updateControl(id, clientId); } @@ -1058,196 +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; - } - - String json; - DynamicJsonDocument document(jsonUpdateDocumentSize); - JsonObject root = document.to(); - - root["type"] = (int)ControlType::GraphPoint; - root["value"] = nValue; - root["id"] = control->id; - serializeJson(document, json); - -#if defined(DEBUG_ESPUI) - if (this->verbosity >= Verbosity::VerboseJSON) - { - Serial.println(json); - } -#endif - - 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; - - for (size_t count = 0; count < this->ws->count();) - { - if (this->ws->hasClient(tryId)) + Control* control = getControl(id); + if (!control) { - if (clientId != tryId) - { - this->ws->client(tryId)->text(json); - -#if defined(DEBUG_ESPUI) - if (this->verbosity >= Verbosity::VerboseJSON) - { - Serial.println(json); - } -#endif - } - - count++; + break; } - tryId++; - } + DynamicJsonDocument document(jsonUpdateDocumentSize); + JsonObject root = document.to(); + + root[F("type")] = (int)ControlType::GraphPoint; + root[F("value")] = nValue; + root[F("id")] = control->id; + + SendJsonDocToWebSocket(document, clientId); + + } while(false); } -/* -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) +bool ESPUIClass::SendJsonDocToWebSocket(ArduinoJson::DynamicJsonDocument& document, uint16_t clientId) { - if(startidx >= this->controlCount) + bool Response = false; + + if(0 > clientId) { - return; + if(MapOfClients.end() != MapOfClients.find(clientId)) + { + Response = MapOfClients[clientId]->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); - - 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); + { + for(auto CurrentClient : MapOfClients) + { + Response |= CurrentClient.second->SendJsonDocToWebSocket(document); + } + } + + return Response; } -/* 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) +void ESPUIClass::jsonDom(uint16_t, AsyncWebSocketClient*, bool) { - //First check that there will be sufficient nodes in the list - if(startindex >= this->controlCount) + NotifyClients(ClientUpdateType_t::RebuildNeeded); +} + +// Tell all of the clients that they need to ask for an upload of the control data. +void ESPUIClass::NotifyClients(ClientUpdateType_t newState) +{ + 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->inputType.length()) - item["inputType"] = String(control->inputType); - 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; + } + } + + if(CanClearUpdateFlags) + { + Control* control = controls; + while(nullptr != control) + { + control->HasBeenSynchronized(); + control = control->next; } - - // special case for selects: to preselect an option, you have to add - // "selected" to