diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fa1385d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* -text diff --git a/.gitignore b/.gitignore index 63123fb..8395b74 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_store +.vscode/c_cpp_properties.json diff --git a/README.md b/README.md index a861d0e..f43b7ae 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -This build is based on: https://github.com/mariusmotea/diyHue/tree/master/Lights/Arduino/Generic_Dimmable_Light +This project is based on: https://github.com/mariusmotea/diyHue/tree/master/Lights/Arduino/Generic_Dimmable_Light + +Most of the code was generated by ChatGPT. The controller I use is a old NodeMCU v1.0, which is connected to 4x D4184 MOS breakout boards. See NodeMCU pins table beyond. The cable I use is a https://amzn.eu/d/0jigRCh. I cut it in half and attached it to the MOS boards. diff --git a/firmware/data/bottom.js b/firmware/data/bottom.js new file mode 100644 index 0000000..976c0d9 --- /dev/null +++ b/firmware/data/bottom.js @@ -0,0 +1,319 @@ +function addTabListener() { +try { +var tabMain = document.getElementById("tab-lights"); +var tabConfig = document.getElementById("tab-config"); +var tabTDE = document.getElementById("tab-tde"); +var amain = document.getElementById("tab-a-lights"); +var acfg = document.getElementById("tab-a-config"); +var atde = document.getElementById("tab-a-tde"); +amain.addEventListener("click", function() { +tabMain.classList.add("visible"); +tabConfig.classList.remove("visible"); +tabTDE.classList.remove("visible"); +amain.classList.add("pure-button-primary"); +acfg.classList.remove("pure-button-primary"); +atde.classList.remove("pure-button-primary"); +}); +acfg.addEventListener("click", function() { +tabMain.classList.remove("visible"); +tabConfig.classList.add("visible"); +tabTDE.classList.remove("visible"); +amain.classList.remove("pure-button-primary"); +acfg.classList.add("pure-button-primary"); +atde.classList.remove("pure-button-primary"); +}); +atde.addEventListener("click", function() { +tabMain.classList.remove("visible"); +tabConfig.classList.remove("visible"); +tabTDE.classList.add("visible"); +amain.classList.remove("pure-button-primary"); +acfg.classList.remove("pure-button-primary"); +atde.classList.add("pure-button-primary"); +createTable(); +fillTableFromJson(); +}); +} catch (error) { +console.log("Error: load listener of the tab action listener management: " + error.message); +} +} +window.addEventListener('load', function() { +addTabListener(); +}); +function loadGraphData() { +console.log('----> generate graph <----'); +$.getJSON('/tc_data_blocks_read', function(data) { +var currenttime = []; +var time = []; +var channel1 = []; +var channel2 = []; +var channel3 = []; +var channel4 = []; +for (var i = 0; i < data['tcdata'].length; i++) { +time.push(data['tcdata'][i]['hour'] + ':' + (data['tcdata'][i]['min'] < 10 ? '0' : '') + data['tcdata'][i]['min']); +channel1.push(data['tcdata'][i]['ch1']); +channel2.push(data['tcdata'][i]['ch2']); +channel3.push(data['tcdata'][i]['ch3']); +channel4.push(data['tcdata'][i]['ch4']); +} +currenttime.push(data['currenttime']['hour']); +currenttime.push(data['currenttime']['min']); +var currentTimeStr = currenttime[0] + ':' + (currenttime[1] < 10 ? '0' : '') + currenttime[1]; +var index = time.indexOf(currentTimeStr); +if (index === -1) { +var lowerIndex = -1; +var upperIndex = -1; +for (var i = 0; i < time.length - 1; i++) { +const currentDate = new Date(); +const year = currentDate.getFullYear(); +const month = (currentDate.getMonth() + 1).toString().padStart(2, '0'); +const day = currentDate.getDate().toString().padStart(2, '0'); +const dateString = `${year}-${month}-${day}`; +const start = moment(dateString + ' ' + time[i], 'YYYY-MM-DD HH:mm'); +const curr = moment(dateString + ' ' + currentTimeStr, 'YYYY-MM-DD HH:mm'); +const end = moment(dateString + ' ' + time[i + 1], 'YYYY-MM-DD HH:mm'); +if (curr.isBetween(start, end)) { +lowerIndex = i; +upperIndex = i + 1; +break; +} +} +if (lowerIndex === -1 || upperIndex === -1) { +console.log("Error: Current time not found in time array and not between two elements in time array. Fixing it..."); +lowerIndex = 0; +upperIndex = 1; +var tmp1 = time[0].split(':'); +currenttime[0] = tmp1[0]; +currenttime[1] = tmp1[1]; +} +var lowerTime = time[lowerIndex].split(":"); +var upperTime = time[upperIndex].split(":"); +var timeDiff = (currenttime[0] - lowerTime[0]) + ((currenttime[1] - lowerTime[1]) / 60); +var indexFloat = lowerIndex + timeDiff / ((upperTime[0] - lowerTime[0]) + ((upperTime[1] - lowerTime[1]) / 60)); +} else { +} +if (indexFloat > index) { +index = indexFloat; +} +var trace1 = { +x: time, +y: channel1, +name: 'Channel 1', +type: 'scatter', +mode: 'lines+markers', +}; +var trace2 = { +x: time, +y: channel2, +name: 'Channel 2', +type: 'scatter', +mode: 'lines+markers', +}; +var trace3 = { +x: time, +y: channel3, +name: 'Channel 3', +type: 'scatter', +mode: 'lines+markers', +}; +var trace4 = { +x: time, +y: channel4, +name: 'Channel 4', +type: 'scatter', +mode: 'lines+markers', +}; +var layout = { +title: 'Timing Control Data Blocks', +xaxis: { +title: 'Time', +tickangle: -45, +}, +yaxis: { +title: 'Brightness', +range: [0, 255], +}, +shapes: [{ +type: 'line', +x0: index, +y0: 0, +x1: index, +y1: 255, +line: { +color: 'lightgrey', +width: 3, +dash: 'dot' +} +}] +}; +Plotly.newPlot('plot_chart', [trace1, trace2, trace3, trace4], layout); +}); +} +setInterval(loadGraphData, 10000); +loadGraphData(); +function updateLightState() { +console.log('----> setting bri and power switch <----'); +for (let i = 1; i <= {{LIGHT_COUNT}}; i++) { +const lightURL = `http://{{IP_ADDRESS}}/state?light=${i}`; +fetch(lightURL).then(response => response.json()).then(data => { +const briSlider = document.getElementById(`bri${i - 1}`); +const briSliderVal = document.getElementById(`bri${i - 1}_val`); +const onLinkOn = document.getElementById(`on${i - 1}_on`); +const onLinkOff = document.getElementById(`on${i - 1}_off`); +briSlider.value = data.bri; +briSliderVal.innerHTML = (Math.round((data.bri * 100.0 / 255.0) * 100) / 100).toFixed(2); +if (data.on == true) { +onLinkOn.classList.add('pure-button-primary'); +onLinkOff.classList.remove('pure-button-primary'); +} else { +onLinkOn.classList.remove('pure-button-primary'); +onLinkOff.classList.add('pure-button-primary'); +} +}).catch(error => console.error(error)); +} +} +setInterval(updateLightState, 10000); +updateLightState(); +function updatePWMValues() { +console.log('----> setting pwm data <----'); +for (let i = 0; i < {{LIGHT_COUNT}}; i++) { +const lightID = i + 1; +const pwmElement = document.getElementById(`light${i}_pwm`); +const pwmElementTxt = document.getElementById(`light${i}_pwm_txt`); +if (pwmElement) { +const url = `http://{{IP_ADDRESS}}/state?light=${lightID}`; +fetch(url).then(response => response.json()).then(data => { +const pwmValue = ((Math.round((data.curpwm - ((data.curpwm >= {{PWM_MIN}}) ? {{PWM_MIN}} : 0)) / {{PWM_MAX}} * 10000) / 100).toFixed(2)); +pwmElement.innerText = pwmValue.toString(); +pwmElement.value = pwmValue; +pwmElementTxt.innerText = pwmValue.toString(); +}).catch(error => console.error(error)); +} +} +} +updatePWMValues(); +setInterval(updatePWMValues, 5000); +var links = document.querySelectorAll('[id^="on"][id$="_off"]'); +links.forEach(function(link) { +link.addEventListener('click', function(event) { +event.preventDefault(); +var id = this.id.replace('on', '').replace('_off', ''); +var xhr = new XMLHttpRequest(); +xhr.open('GET', 'http://{{IP_ADDRESS}}/?on' + id + '=false&transition=' + document.getElementById('transition').value, true); +xhr.send(); +updateLightState(); +this.classList.add('pure-button-primary'); +document.getElementById('on'+id+'_on').classList.remove('pure-button-primary'); +}); +}); +var links = document.querySelectorAll('[id^="on"][id$="_on"]'); +links.forEach(function(link) { +link.addEventListener('click', function(event) { +event.preventDefault(); +var id = this.id.replace('on', '').replace('_on', ''); +var xhr = new XMLHttpRequest(); +xhr.open('GET', 'http://{{IP_ADDRESS}}/?on' + id + '=true&transition=' + document.getElementById('transition').value, true); +xhr.send(); +updateLightState(); +this.classList.add('pure-button-primary'); +document.getElementById('on'+id+'_off').classList.remove('pure-button-primary'); +}); +}); +function createTable() { +var table = document.createElement("table"); +table.border = "1"; +var headerRow = document.createElement("tr"); +var headers = ["Hour", "Minute", "Ch1", "Ch2", "Ch3", "Ch4"]; +headers.forEach(header => { +var th = document.createElement("th"); +th.innerHTML = header; +headerRow.appendChild(th); +}); +table.appendChild(headerRow); +for (var i = 1; i <= 10; i++) { +var tr = document.createElement("tr"); +var tdHour = createSelectCell(23, 0); +tdHour.id = "hour" + i; +tr.appendChild(tdHour); +var tdMinute = createSelectCell(59, 0); +tdMinute.id = "minute" + i; +tr.appendChild(tdMinute); +var tdCh1 = createSelectCell(100, 0); +tdCh1.id = "ch1_" + i; +tr.appendChild(tdCh1); +var tdCh2 = createSelectCell(100, 0); +tdCh2.id = "ch2_" + i; +tr.appendChild(tdCh2); +var tdCh3 = createSelectCell(100, 0); +tdCh3.id = "ch3_" + i; +tr.appendChild(tdCh3); +var tdCh4 = createSelectCell(100, 0); +tdCh4.id = "ch4_" + i; +tr.appendChild(tdCh4); +table.appendChild(tr); +} +var container = document.getElementById("table-container"); +container.innerHTML = ""; +container.classList.add("pure-form"); +container.appendChild(table); +} +function createSelectCell(max, value) { +var select = document.createElement("select"); +for (var i = 0; i <= max; i++) { +var option = document.createElement("option"); +option.value = i; +option.text = i; +select.appendChild(option); +} +select.value = value; +var row = document.createElement("td"); +row.appendChild(select); +return row; +} +function fillTableFromJson() { +fetch('http://{{IP_ADDRESS}}/tc_data_blocks_read') +.then(response => response.json()) +.then(data => { +var tcdata = data.tcdata; +for (var i = 0; i < tcdata.length; i++) { +var row = document.getElementById("hour" + (i+1)).parentNode; +row.cells[0].childNodes[0].value = tcdata[i].hour; +row.cells[1].childNodes[0].value = tcdata[i].min; +row.cells[2].childNodes[0].value = parseInt(tcdata[i].ch1 * 100 / 255); +row.cells[3].childNodes[0].value = parseInt(tcdata[i].ch2 * 100 / 255); +row.cells[4].childNodes[0].value = parseInt(tcdata[i].ch3 * 100 / 255); +row.cells[5].childNodes[0].value = parseInt(tcdata[i].ch4 * 100 / 255); +} +}); +} +function createJsonFromTable() { +var tableRows = document.querySelectorAll("table tr"); +var tcdata = []; +for (var i = 1; i <= 10; i++) { +var row = document.getElementById("hour" + i).parentNode; +var hour = parseInt(row.cells[0].childNodes[0].value); +var min = parseInt(row.cells[1].childNodes[0].value); +var ch1 = Math.round(parseInt(row.cells[2].childNodes[0].value) * 2.55); +var ch2 = Math.round(parseInt(row.cells[3].childNodes[0].value) * 2.55); +var ch3 = Math.round(parseInt(row.cells[4].childNodes[0].value) * 2.55); +var ch4 = Math.round(parseInt(row.cells[5].childNodes[0].value) * 2.55); +tcdata.push({hour: hour, min: min, ch1: ch1, ch2: ch2, ch3: ch3, ch4: ch4}); +} +var currentTime = {hour: new Date().getHours(), min: new Date().getMinutes()}; +var jsonData = {tcdata: tcdata, currenttime: currentTime}; +console.log("jsonData = " + JSON.stringify(jsonData)); +return JSON.stringify(jsonData); +} +function sendDataToServer() { +var jsonData = createJsonFromTable(); +var urlEncodedData = encodeURIComponent(jsonData); +var url = 'http://{{IP_ADDRESS}}/tc_data_blocks_store?data=' + urlEncodedData; +console.log("url so save = " + url); +var xhr = new XMLHttpRequest(); +xhr.open('GET', url, true); +xhr.onreadystatechange = function() { +if (xhr.readyState === 4 && xhr.status === 200) { +console.log('Data successfully sent to server!'); +} +}; +xhr.send(); +} diff --git a/firmware/data/config_template.html b/firmware/data/config_template.html index da7ca3b..820b72a 100644 --- a/firmware/data/config_template.html +++ b/firmware/data/config_template.html @@ -1,70 +1,70 @@ -
-
-
- -
- - -
-
- - -
-
- -
- - -
-
- - -
-
- -
- -ON -OFF -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
+
+
+
+ +
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ +
+ +ON +OFF +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
diff --git a/firmware/data/index_template_bottom.html b/firmware/data/index_template_bottom.html index 32db70c..c1156c1 100644 --- a/firmware/data/index_template_bottom.html +++ b/firmware/data/index_template_bottom.html @@ -1,254 +1,20 @@ - - - - - - - + +
+
+
+ +
+
+
+
+save +
+
+ + + + + diff --git a/firmware/data/index_template_middle.html b/firmware/data/index_template_middle.html index 90d1ffc..6155028 100644 --- a/firmware/data/index_template_middle.html +++ b/firmware/data/index_template_middle.html @@ -1,12 +1,8 @@ - - - -
- - - - - -
-Not implemented yet. -
+ + + +
+ + + + diff --git a/firmware/data/index_template_top.html b/firmware/data/index_template_top.html index c306e26..7f237d0 100644 --- a/firmware/data/index_template_top.html +++ b/firmware/data/index_template_top.html @@ -1,108 +1,50 @@ - - - - - -Light setup - {{LIGHT_NAME}} - - - - - - - - -
-

{{LIGHT_NAME}}

-
- -
-
-Lights control -Config -Timing data edit -
-
-
-
-
- -ON -OFF -
- - -
-
- -
- - - + + +
- + + + + + +Light setup - {{LIGHT_NAME}} + + + + + + + + + +
+

{{LIGHT_NAME}}

+
+ +
+ +
+
+
+
+ +ON +OFF +
+ + +
+
+ +
+ + + - - -
diff --git a/firmware/data/light_control_template.html b/firmware/data/light_control_template.html index 8bc313f..88e7f86 100644 --- a/firmware/data/light_control_template.html +++ b/firmware/data/light_control_template.html @@ -1,29 +1,29 @@ -

Light {{LIGHT_NUMBER}}

-
- -ON -OFF -
-
- - -  -9 -% -
- - -  - -% - -
+

Light {{LIGHT_NUMBER}}

+
+ +ON +OFF +
+
+ + +  +9 +% +
+ + +  + +% + +
diff --git a/firmware/data/style.css b/firmware/data/style.css new file mode 100644 index 0000000..e39a471 --- /dev/null +++ b/firmware/data/style.css @@ -0,0 +1,43 @@ +#tab-lights, #tab-config, #tab-tde { +display: none; +background-color: #ffffff; +color: black; +font-weight: bold; +} +#tab-lights.visible { +display: block; +} +#tab-config.visible { +display: block; +} +#tab-tde.visible { +display: block; +} +.pure-table td { +padding: 4px; +} +.pure-form input[type="number"] { +width: 60px; +height: 20px; +border-radius: 3px; +border: 1px solid #ccc; +} +.pure-form select { +width: 80px; +height: 26px; +border-radius: 3px; +border: 1px solid #ccc; +background-color: #fff; +} +.pure-button { +background-color: #5a5a5a; +color: #fff; +border-radius: 3px; +border: none; +padding: 8px 12px; +font-size: 14px; +cursor: pointer; +} +.pure-button:hover { +background-color: #333; +} \ No newline at end of file diff --git a/firmware/data/top.js b/firmware/data/top.js new file mode 100644 index 0000000..373f487 --- /dev/null +++ b/firmware/data/top.js @@ -0,0 +1,38 @@ +var links = document.querySelectorAll('[id^="tc_on"]'); +links.forEach(function(link) { +link.addEventListener('click', function(event) { +event.preventDefault(); +var xhr = new XMLHttpRequest(); +xhr.open('GET', 'http://{{IP_ADDRESS}}/?tc=true', true); +xhr.send(); +document.getElementById('tc_on').classList.add('pure-button-primary'); +document.getElementById('tc_off').classList.remove('pure-button-primary'); +}); +}); +var links = document.querySelectorAll('[id^="tc_off"]'); +links.forEach(function(link) { +link.addEventListener('click', function(event) { +event.preventDefault(); +var xhr = new XMLHttpRequest(); +xhr.open('GET', 'http://{{IP_ADDRESS}}/?tc=false', true); +xhr.send(); +document.getElementById('tc_off').classList.add('pure-button-primary'); +document.getElementById('tc_on').classList.remove('pure-button-primary'); +}); +}); +let timeoutId; +function sendSliderValue(x) { +x = x - 1; +clearTimeout(timeoutId); +timeoutId = setTimeout(() => { +var value = document.getElementById(`bri${x}`).value; +var url = `http://{{IP_ADDRESS}}/?bri${x}=${value}`; +fetch(url).then(response => { +if (!response.ok) { +throw new Error(`HTTP error! status: ${response.status}`); +} +}).catch(error => { +console.error(`Error sending slider value to ${url}: ${error}`); +}); +}, 500); +} \ No newline at end of file diff --git a/firmware/firmware.ino b/firmware/firmware.ino index 8b5e176..dda1e36 100644 --- a/firmware/firmware.ino +++ b/firmware/firmware.ino @@ -472,13 +472,39 @@ void init_webserver() server.send(200, "text/plain", output); }); - server.on("/tc_data_blocks", []() + server.on("/tc_data_blocks_read", []() { String output = tc_getJsonData(); server.send(200, "application/json", output); }); + server.on("/tc_data_blocks_store", []() + { + if (server.hasArg("data")) + { + String jsonData = server.arg("data"); + Serial.println("Received: " + jsonData); + tc_jsonDataBlocksToEEPROM(jsonData); + server.send(200, "text/html", "tcdata saved"); + } + }); + + server.on("/js_top", []() + { + server.send(200, "text/html", replacePlaceholder(loadSPIFFSFile("/top.js"))); + }); + + server.on("/js_bottom", []() + { + server.send(200, "text/html", replacePlaceholder(loadSPIFFSFile("/bottom.js"))); + }); + + server.on("/css", []() + { + server.send(200, "text/css", loadSPIFFSFile("/style.css")); + }); + server.on("/", []() { @@ -703,6 +729,11 @@ String genConfigHTML() return replacePlaceholder(getConfigHTML()); } +String genTCEditHTML() +{ + return replacePlaceholder(getTCDataEditHTML()); +} + String genLightControlHTML() { String http_content = ""; @@ -868,6 +899,14 @@ String getConfigHTML() //********************************// +String getTCDataEditHTML() +{ + // load file + return loadSPIFFSFile("/tc_data_edit.html"); +} + +//********************************// + String getLightControlHTML() { // load file diff --git a/firmware/html/bottom.js b/firmware/html/bottom.js new file mode 100644 index 0000000..c19e1bd --- /dev/null +++ b/firmware/html/bottom.js @@ -0,0 +1,401 @@ +function addTabListener() { + //console.log("Try to add tab listener"); + try { + var tabMain = document.getElementById("tab-lights"); + var tabConfig = document.getElementById("tab-config"); + var tabTDE = document.getElementById("tab-tde"); + + var amain = document.getElementById("tab-a-lights"); + var acfg = document.getElementById("tab-a-config"); + var atde = document.getElementById("tab-a-tde"); + + + amain.addEventListener("click", function() { + //console.log("Switch to main lights tab"); + tabMain.classList.add("visible"); + tabConfig.classList.remove("visible"); + tabTDE.classList.remove("visible"); + + amain.classList.add("pure-button-primary"); + acfg.classList.remove("pure-button-primary"); + atde.classList.remove("pure-button-primary"); + }); + + acfg.addEventListener("click", function() { + //console.log("Switch to config tab"); + tabMain.classList.remove("visible"); + tabConfig.classList.add("visible"); + tabTDE.classList.remove("visible"); + + amain.classList.remove("pure-button-primary"); + acfg.classList.add("pure-button-primary"); + atde.classList.remove("pure-button-primary"); + }); + + atde.addEventListener("click", function() { + //console.log("Switch to TDE tab"); + tabMain.classList.remove("visible"); + tabConfig.classList.remove("visible"); + tabTDE.classList.add("visible"); + + amain.classList.remove("pure-button-primary"); + acfg.classList.remove("pure-button-primary"); + atde.classList.add("pure-button-primary"); + + createTable(); // recreate the table on timing data editor tab + fillTableFromJson(); + }); + } catch (error) { + console.log("Error: load listener of the tab action listener management: " + error.message); + } +} +window.addEventListener('load', function() { + addTabListener(); +}); + +function loadGraphData() { + console.log('----> generate graph <----'); + $.getJSON('/tc_data_blocks_read', function(data) { + var currenttime = []; + var time = []; + var channel1 = []; + var channel2 = []; + var channel3 = []; + var channel4 = []; + for (var i = 0; i < data['tcdata'].length; i++) { + time.push(data['tcdata'][i]['hour'] + ':' + (data['tcdata'][i]['min'] < 10 ? '0' : '') + data['tcdata'][i]['min']); + channel1.push(data['tcdata'][i]['ch1']); + channel2.push(data['tcdata'][i]['ch2']); + channel3.push(data['tcdata'][i]['ch3']); + channel4.push(data['tcdata'][i]['ch4']); + } + currenttime.push(data['currenttime']['hour']); + currenttime.push(data['currenttime']['min']); + //console.log(currenttime); + var currentTimeStr = currenttime[0] + ':' + (currenttime[1] < 10 ? '0' : '') + currenttime[1]; + var index = time.indexOf(currentTimeStr); + if (index === -1) { + var lowerIndex = -1; + var upperIndex = -1; + for (var i = 0; i < time.length - 1; i++) { + //console.log(time[i] + ' <= ' + currentTimeStr + ' >= ' + time[i + 1]); + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = (currentDate.getMonth() + 1).toString().padStart(2, '0'); + const day = currentDate.getDate().toString().padStart(2, '0'); + const dateString = `${year}-${month}-${day}`; + const start = moment(dateString + ' ' + time[i], 'YYYY-MM-DD HH:mm'); + const curr = moment(dateString + ' ' + currentTimeStr, 'YYYY-MM-DD HH:mm'); + const end = moment(dateString + ' ' + time[i + 1], 'YYYY-MM-DD HH:mm'); + //console.log(start.format('YYYY-MM-DD HH:mm') + ' <= ' + curr.format('YYYY-MM-DD HH:mm') + ' >= ' + end.format('YYYY-MM-DD HH:mm')); + //console.log(curr.isBetween(start, end)); + if (curr.isBetween(start, end)) { + lowerIndex = i; + upperIndex = i + 1; + break; + } + } + //console.log('lowerIndex=' + lowerIndex); + //console.log('upperIndex=' + upperIndex); + if (lowerIndex === -1 || upperIndex === -1) { + console.log("Error: Current time not found in time array and not between two elements in time array. Fixing it..."); + lowerIndex = 0; + upperIndex = 1; + var tmp1 = time[0].split(':'); + //console.log('tmp1 = ' + tmp1); + currenttime[0] = tmp1[0]; + currenttime[1] = tmp1[1]; + } + var lowerTime = time[lowerIndex].split(":"); + var upperTime = time[upperIndex].split(":"); + var timeDiff = (currenttime[0] - lowerTime[0]) + ((currenttime[1] - lowerTime[1]) / 60); + var indexFloat = lowerIndex + timeDiff / ((upperTime[0] - lowerTime[0]) + ((upperTime[1] - lowerTime[1]) / 60)); + //console.log("Index (float): " + indexFloat); + } else { + //console.log("Index (integer): " + index); + //console.log("Index (float): " + index); + } + if (indexFloat > index) { + index = indexFloat; + } + //console.log("index in graph >>>" + index); + var trace1 = { + x: time, + y: channel1, + name: 'Channel 1', + type: 'scatter', + mode: 'lines+markers', + }; + var trace2 = { + x: time, + y: channel2, + name: 'Channel 2', + type: 'scatter', + mode: 'lines+markers', + }; + var trace3 = { + x: time, + y: channel3, + name: 'Channel 3', + type: 'scatter', + mode: 'lines+markers', + }; + var trace4 = { + x: time, + y: channel4, + name: 'Channel 4', + type: 'scatter', + mode: 'lines+markers', + }; + var layout = { + title: 'Timing Control Data Blocks', + xaxis: { + title: 'Time', + tickangle: -45, + }, + yaxis: { + title: 'Brightness', + range: [0, 255], + }, + shapes: [{ + type: 'line', + x0: index, + y0: 0, + x1: index, + y1: 255, + line: { + color: 'lightgrey', + width: 3, + dash: 'dot' + } + }] + }; + Plotly.newPlot('plot_chart', [trace1, trace2, trace3, trace4], layout); + }); +} +setInterval(loadGraphData, 10000); +loadGraphData(); + +function updateLightState() { + console.log('----> setting bri and power switch <----'); + for (let i = 1; i <= {{LIGHT_COUNT}}; i++) { + const lightURL = `http://{{IP_ADDRESS}}/state?light=${i}`; + fetch(lightURL).then(response => response.json()).then(data => { + const briSlider = document.getElementById(`bri${i - 1}`); + const briSliderVal = document.getElementById(`bri${i - 1}_val`); + const onLinkOn = document.getElementById(`on${i - 1}_on`); + const onLinkOff = document.getElementById(`on${i - 1}_off`); + briSlider.value = data.bri; + briSliderVal.innerHTML = (Math.round((data.bri * 100.0 / 255.0) * 100) / 100).toFixed(2); + //console.log('data.on ' + i + ' = ' + data.on); + if (data.on == true) { + onLinkOn.classList.add('pure-button-primary'); + onLinkOff.classList.remove('pure-button-primary'); + } else { + onLinkOn.classList.remove('pure-button-primary'); + onLinkOff.classList.add('pure-button-primary'); + } + }).catch(error => console.error(error)); + } +} +setInterval(updateLightState, 10000); +updateLightState(); + +function updatePWMValues() { + console.log('----> setting pwm data <----'); + for (let i = 0; i < {{LIGHT_COUNT}}; i++) { + const lightID = i + 1; + const pwmElement = document.getElementById(`light${i}_pwm`); + const pwmElementTxt = document.getElementById(`light${i}_pwm_txt`); + if (pwmElement) { + const url = `http://{{IP_ADDRESS}}/state?light=${lightID}`; + fetch(url).then(response => response.json()).then(data => { + const pwmValue = ((Math.round((data.curpwm - ((data.curpwm >= {{PWM_MIN}}) ? {{PWM_MIN}} : 0)) / {{PWM_MAX}} * 10000) / 100).toFixed(2)); + //console.log('curpwm[' + i + '] = ' + data.curpwm + ' = ' + pwmValue); + pwmElement.innerText = pwmValue.toString(); + pwmElement.value = pwmValue; + pwmElementTxt.innerText = pwmValue.toString(); + }).catch(error => console.error(error)); + } + } +} +updatePWMValues(); +setInterval(updatePWMValues, 5000); + +// Suche nach allen Links auf der Seite mit IDs von on0_off bis on3_off +var links = document.querySelectorAll('[id^="on"][id$="_off"]'); +// Füge einen Klick-Listener zu jedem Link hinzu +links.forEach(function(link) { + link.addEventListener('click', function(event) { + // Verhindere, dass der Link die Seite neu lädt + event.preventDefault(); + // Extrahiere die Zahl aus der ID des Links + var id = this.id.replace('on', '').replace('_off', ''); + // Erstelle eine neue Anfrage an die entsprechende URL + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://{{IP_ADDRESS}}/?on' + id + '=false&transition=' + document.getElementById('transition').value, true); + // Sende die Anfrage im Hintergrund + xhr.send(); + updateLightState(); + this.classList.add('pure-button-primary'); + document.getElementById('on'+id+'_on').classList.remove('pure-button-primary'); + }); +}); +// Suche nach allen Links auf der Seite mit IDs von on0_off bis on3_off +var links = document.querySelectorAll('[id^="on"][id$="_on"]'); +// Füge einen Klick-Listener zu jedem Link hinzu +links.forEach(function(link) { + link.addEventListener('click', function(event) { + // Verhindere, dass der Link die Seite neu lädt + event.preventDefault(); + // Extrahiere die Zahl aus der ID des Links + var id = this.id.replace('on', '').replace('_on', ''); + // Erstelle eine neue Anfrage an die entsprechende URL + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://{{IP_ADDRESS}}/?on' + id + '=true&transition=' + document.getElementById('transition').value, true); + // Sende die Anfrage im Hintergrund + xhr.send(); + updateLightState(); + this.classList.add('pure-button-primary'); + document.getElementById('on'+id+'_off').classList.remove('pure-button-primary'); + }); +}); + +function createTable() { + // Erstelle eine Tabelle + var table = document.createElement("table"); + table.border = "1"; + + // Erstelle eine Kopfzeile + var headerRow = document.createElement("tr"); + + // Füge die Spaltenüberschriften hinzu + var headers = ["Hour", "Minute", "Ch1", "Ch2", "Ch3", "Ch4"]; + headers.forEach(header => { + var th = document.createElement("th"); + th.innerHTML = header; + headerRow.appendChild(th); + }); + table.appendChild(headerRow); + + // Erstelle Zeilen mit Zellen für jedes Element + for (var i = 1; i <= 10; i++) { + var tr = document.createElement("tr"); + + // Stunde + var tdHour = createSelectCell(23, 0); + tdHour.id = "hour" + i; + tr.appendChild(tdHour); + + // Minute + var tdMinute = createSelectCell(59, 0); + tdMinute.id = "minute" + i; + tr.appendChild(tdMinute); + + // ch1 + var tdCh1 = createSelectCell(100, 0); + tdCh1.id = "ch1_" + i; + tr.appendChild(tdCh1); + + // ch2 + var tdCh2 = createSelectCell(100, 0); + tdCh2.id = "ch2_" + i; + tr.appendChild(tdCh2); + + // ch3 + var tdCh3 = createSelectCell(100, 0); + tdCh3.id = "ch3_" + i; + tr.appendChild(tdCh3); + + // ch4 + var tdCh4 = createSelectCell(100, 0); + tdCh4.id = "ch4_" + i; + tr.appendChild(tdCh4); + + table.appendChild(tr); + } + + var container = document.getElementById("table-container"); + container.innerHTML = ""; + container.classList.add("pure-form"); + container.appendChild(table); +} + +function createSelectCell(max, value) { + // Erstelle ein neues select-Element + var select = document.createElement("select"); + + // Füge Optionen hinzu + for (var i = 0; i <= max; i++) { + var option = document.createElement("option"); + option.value = i; + option.text = i; + select.appendChild(option); + } + + // Wähle die Option aus, die dem übergebenen Wert entspricht + select.value = value; + + // Erstelle eine neue Zellenreihe und -zelle + var row = document.createElement("td"); + row.appendChild(select); + + // Gib die Zelle zurück + return row; +} + +function fillTableFromJson() { + // Lade JSON-Daten + fetch('http://{{IP_ADDRESS}}/tc_data_blocks_read') + .then(response => response.json()) + .then(data => { + // Fülle die Tabelle mit Daten + var tcdata = data.tcdata; + for (var i = 0; i < tcdata.length; i++) { + var row = document.getElementById("hour" + (i+1)).parentNode; + row.cells[0].childNodes[0].value = tcdata[i].hour; + row.cells[1].childNodes[0].value = tcdata[i].min; + row.cells[2].childNodes[0].value = parseInt(tcdata[i].ch1 * 100 / 255); + row.cells[3].childNodes[0].value = parseInt(tcdata[i].ch2 * 100 / 255); + row.cells[4].childNodes[0].value = parseInt(tcdata[i].ch3 * 100 / 255); + row.cells[5].childNodes[0].value = parseInt(tcdata[i].ch4 * 100 / 255); + } + }); +} + +function createJsonFromTable() { + var tableRows = document.querySelectorAll("table tr"); + var tcdata = []; + for (var i = 1; i <= 10; i++) { + + var row = document.getElementById("hour" + i).parentNode; + var hour = parseInt(row.cells[0].childNodes[0].value); + var min = parseInt(row.cells[1].childNodes[0].value); + var ch1 = Math.round(parseInt(row.cells[2].childNodes[0].value) * 2.55); + var ch2 = Math.round(parseInt(row.cells[3].childNodes[0].value) * 2.55); + var ch3 = Math.round(parseInt(row.cells[4].childNodes[0].value) * 2.55); + var ch4 = Math.round(parseInt(row.cells[5].childNodes[0].value) * 2.55); + + tcdata.push({hour: hour, min: min, ch1: ch1, ch2: ch2, ch3: ch3, ch4: ch4}); + } + var currentTime = {hour: new Date().getHours(), min: new Date().getMinutes()}; + var jsonData = {tcdata: tcdata, currenttime: currentTime}; + console.log("jsonData = " + JSON.stringify(jsonData)); + return JSON.stringify(jsonData); +} + +function sendDataToServer() { + var jsonData = createJsonFromTable(); + var urlEncodedData = encodeURIComponent(jsonData); + var url = 'http://{{IP_ADDRESS}}/tc_data_blocks_store?data=' + urlEncodedData; + console.log("url so save = " + url); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + console.log('Data successfully sent to server!'); + } + }; + xhr.send(); + } + diff --git a/firmware/html/index_template_bottom.html b/firmware/html/index_template_bottom.html index e2c534a..a72b1b7 100644 --- a/firmware/html/index_template_bottom.html +++ b/firmware/html/index_template_bottom.html @@ -1,254 +1,22 @@ - - - - - - - + +
+
+
+ +
+
+
+
+ save +
+
+ + + + + + + diff --git a/firmware/html/index_template_middle.html b/firmware/html/index_template_middle.html index 247d0ae..2b948ab 100644 --- a/firmware/html/index_template_middle.html +++ b/firmware/html/index_template_middle.html @@ -1,12 +1,8 @@ - -
-
-
-
- -
- Not implemented yet. -
+ +
+
+
+
diff --git a/firmware/html/index_template_top.html b/firmware/html/index_template_top.html index d70d8b7..3ac1cf8 100644 --- a/firmware/html/index_template_top.html +++ b/firmware/html/index_template_top.html @@ -1,108 +1,51 @@ - - - - - - Light setup - {{LIGHT_NAME}} - - - - - - - - -
-

{{LIGHT_NAME}}

-
- -
-
- Lights control - Config - Timing data edit -
-
-
-
-
- - ON - OFF -
- - -
-
- -
- - -
- + + + + + + Light setup - {{LIGHT_NAME}} + + + + + + + + + +
+

{{LIGHT_NAME}}

+
+ +
+ +
+
+
+
+ + ON + OFF +
+ + +
+
+ +
+ + +
+ diff --git a/firmware/html/style.css b/firmware/html/style.css new file mode 100644 index 0000000..e39a471 --- /dev/null +++ b/firmware/html/style.css @@ -0,0 +1,43 @@ +#tab-lights, #tab-config, #tab-tde { +display: none; +background-color: #ffffff; +color: black; +font-weight: bold; +} +#tab-lights.visible { +display: block; +} +#tab-config.visible { +display: block; +} +#tab-tde.visible { +display: block; +} +.pure-table td { +padding: 4px; +} +.pure-form input[type="number"] { +width: 60px; +height: 20px; +border-radius: 3px; +border: 1px solid #ccc; +} +.pure-form select { +width: 80px; +height: 26px; +border-radius: 3px; +border: 1px solid #ccc; +background-color: #fff; +} +.pure-button { +background-color: #5a5a5a; +color: #fff; +border-radius: 3px; +border: none; +padding: 8px 12px; +font-size: 14px; +cursor: pointer; +} +.pure-button:hover { +background-color: #333; +} \ No newline at end of file diff --git a/firmware/html/top.js b/firmware/html/top.js new file mode 100644 index 0000000..73dd43b --- /dev/null +++ b/firmware/html/top.js @@ -0,0 +1,42 @@ +var links = document.querySelectorAll('[id^="tc_on"]'); +links.forEach(function(link) { + link.addEventListener('click', function(event) { + event.preventDefault(); + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://{{IP_ADDRESS}}/?tc=true', true); + xhr.send(); + //console.log('tc=true call'); + document.getElementById('tc_on').classList.add('pure-button-primary'); + document.getElementById('tc_off').classList.remove('pure-button-primary'); + }); +}); +var links = document.querySelectorAll('[id^="tc_off"]'); +links.forEach(function(link) { + link.addEventListener('click', function(event) { + event.preventDefault(); + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://{{IP_ADDRESS}}/?tc=false', true); + xhr.send(); + //console.log('tc=false call'); + document.getElementById('tc_off').classList.add('pure-button-primary'); + document.getElementById('tc_on').classList.remove('pure-button-primary'); + }); +}); + +let timeoutId; +function sendSliderValue(x) { + x = x - 1; + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + var value = document.getElementById(`bri${x}`).value; + var url = `http://{{IP_ADDRESS}}/?bri${x}=${value}`; + fetch(url).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + //console.log(`Sent slider value ${value} to ${url}`); + }).catch(error => { + console.error(`Error sending slider value to ${url}: ${error}`); + }); + }, 500); +} \ No newline at end of file diff --git a/firmware/timing_control.ino b/firmware/timing_control.ino index 304e9e5..64ace7c 100644 --- a/firmware/timing_control.ino +++ b/firmware/timing_control.ino @@ -48,7 +48,7 @@ uint8_t example_timer_data_block[] = { 18, 0, 205, 205, 205, 205, // 4: 80% all for 5 hours 19, 0, 50, 50, 50, 50, // 5: 20% all 19, 30, 50, 0, 50, 0, // 6: 20% all blues - 20, 0, 25, 0, 25, 0, // 9: disabled + 20, 0, 25, 0, 25, 0, // 9: 10% all blues 20, 30, 25, 0, 0, 0, // 7: 10% ch1 blues 21, 0, 0, 0, 0, 0, // 8: 0% all }; @@ -180,6 +180,12 @@ void tc_update_main() if (target_data_block >= NUMBER_OF_TIMER_DATA_BLOCKS) { + // we are not between two valid data points, do nothing + // TODO check if setting all lights to false is correct here + for (uint8_t i = 0; i < LIGHTS_COUNT; i++) + { + light_state[i] = false; + } target_data_block = 255; current_target_data_block = 255; return; @@ -371,7 +377,7 @@ bool tc_check_no_data_block() uint8_t e = EEPROM.read(EEPROM_TIMING_DATA_ADDRESS); //Serial.println(e); - if (e == 255) + if (e > 23) // the maximum value for tis memory section is 23 { return true; } @@ -409,25 +415,90 @@ String tc_getJsonData() void tc_jsonDataBlocksToEEPROM(String json_data_string) { - StaticJsonDocument<512> doc; + StaticJsonDocument<1289> doc; deserializeJson(doc, json_data_string); + Serial.println("Reading data from json data:"); // Loop through each data block in the JSON data and store it in the tc_data array for (uint8_t i = 0; i < NUMBER_OF_TIMER_DATA_BLOCKS; i++) { - JsonObject obj = doc[i]; - tc_data[i].hh = obj["hour"]; - tc_data[i].mm = obj["min"]; - tc_data[i].ch1 = obj["ch1"]; - tc_data[i].ch2 = obj["ch2"]; - tc_data[i].ch3 = obj["ch3"]; - tc_data[i].ch4 = obj["ch4"]; + //Serial.println("<< " + (String)i); + + JsonObject obj = doc["tcdata"][i]; + + // Check and set the limits of the hour value + int hour = obj["hour"]; + if (hour < 0) { + hour = 0; + } else if (hour > 23) { + hour = 23; + } + tc_data[i].hh = hour; + Serial.print("hour = " + (String)tc_data[i].hh + " " + (String)obj["hour"] + " "); + + // Check and set the limits of the minute value + int minute = obj["min"]; + if (minute < 0) { + minute = 0; + } else if (minute > 59) { + minute = 59; + } + tc_data[i].mm = minute; + Serial.print("minute = " + (String)tc_data[i].mm + " " + (String)obj["min"] + " "); + + // Check and set the limits of the ch1 value + int ch1 = obj["ch1"]; + if (ch1 < 0) { + ch1 = 0; + } else if (ch1 > 255) { + ch1 = 255; + } + tc_data[i].ch1 = ch1; + Serial.print("ch1 = " + (String)tc_data[i].ch1 + " " + (String)obj["ch1"] + " "); + + // Check and set the limits of the ch2 value + int ch2 = obj["ch2"]; + if (ch2 < 0) { + ch2 = 0; + } else if (ch2 > 255) { + ch2 = 255; + } + tc_data[i].ch2 = ch2; + Serial.print("ch2 = " + (String)tc_data[i].ch2 + " " + (String)obj["ch2"] + " "); + + // Check and set the limits of the ch3 value + int ch3 = obj["ch3"]; + if (ch3 < 0) { + ch3 = 0; + } else if (ch3 > 255) { + ch3 = 255; + } + tc_data[i].ch3 = ch3; + Serial.print("ch3 = " + (String)tc_data[i].ch3 + " " + (String)obj["ch3"] + " "); + + // Check and set the limits of the ch4 value + int ch4 = obj["ch4"]; + if (ch4 < 0) { + ch4 = 0; + } else if (ch4 > 255) { + ch4 = 255; + } + tc_data[i].ch4 = ch4; + Serial.println("ch4 = " + (String)tc_data[i].ch4 + " " + (String)obj["ch4"] + "\n---"); } + Serial.println("Writing to EEPROM..."); // Write the tc_data array to the EEPROM for (uint16_t i = 0; i < (NUMBER_OF_TIMER_DATA_BLOCKS * LENGTH_OF_TIMER_DATA_BLOCK); i++) { + //Serial.println(" " + (String)(EEPROM_TIMING_DATA_ADDRESS + i) + " " + (String)(*((uint8_t*)&tc_data + i))); + EEPROM.write(EEPROM_TIMING_DATA_ADDRESS + i, *((uint8_t*)&tc_data + i)); + /*if (((i+1) % 6 == 0)) + { + Serial.println("---"); + }*/ + } EEPROM.commit(); } diff --git a/tools/html2string.sh b/tools/html2string.sh index 73c4797..5f4c178 100755 --- a/tools/html2string.sh +++ b/tools/html2string.sh @@ -1,4 +1,10 @@ #!/bin/bash -cat $1 | sed -e"s/^ *//ig" | sed -e "s/^\/\/.*//ig" - +# Remove leading whitespace and comments starting with // +sed -e 's/^[[:space:]]*//' -e '/^\/\/.*/d' $1 | +# Remove one line comments starting with // +sed -e 's/^\/\/.*$//' | +# Remove trailing comments starting with // +sed -e 's/ \/\/.*$//' | +# Remove empty lines +sed '/^\s*$/d' diff --git a/tools/html_gen_files.sh b/tools/html_gen_files.sh index 8665590..61222d8 100755 --- a/tools/html_gen_files.sh +++ b/tools/html_gen_files.sh @@ -5,3 +5,6 @@ bash ../../tools/html2string.sh ../html/index_template_middle.html > index_templ bash ../../tools/html2string.sh ../html/index_template_bottom.html > index_template_bottom.html bash ../../tools/html2string.sh ../html/config_template.html > config_template.html bash ../../tools/html2string.sh ../html/light_control_template.html > light_control_template.html +bash ../../tools/html2string.sh ../html/top.js > top.js +bash ../../tools/html2string.sh ../html/bottom.js > bottom.js +cp -av ../html/style.css .