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 @@ -