document.addEventListener('DOMContentLoaded', () => { M.AutoInit(document.body); }); const colorReplace = (input) => { input = input.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); input = input.replace(/\\033\[(?:0;)?31m/g, ''); input = input.replace(/\\033\[(?:0?1;)?31m/g, ''); input = input.replace(/\\033\[(?:0;)?32m/g, ''); input = input.replace(/\\033\[(?:0?1;)?32m/g, ''); input = input.replace(/\\033\[(?:0;)?33m/g, ''); input = input.replace(/\\033\[(?:0?1;)?33m/g, ''); input = input.replace(/\\033\[(?:0;)?35m/g, ''); input = input.replace(/\\033\[(?:0?1;)?35m/g, ''); input = input.replace(/\\033\[(?:0;)?36m/g, ''); input = input.replace(/\\033\[(?:0?1;)?36m/g, ''); input = input.replace(/\\033\[(?:0;)?37m/g, ''); input = input.replace(/\\033\[(?:0?1;)?37m/g, ''); input = input.replace(/\\033\[(?:0;)?38m/g, ''); input = input.replace(/\\033\[(?:0?1;)?38m/g, ''); input = input.replace(/\\033\[0m/g, ''); return input; }; let configuration = ""; let wsProtocol = "ws:"; if (window.location.protocol === "https:") { wsProtocol = 'wss:'; } const wsUrl = wsProtocol + '//' + window.location.hostname + ':' + window.location.port; let isFetchingPing = false; const fetchPing = () => { if (isFetchingPing) return; isFetchingPing = true; fetch('/ping', {credentials: "same-origin"}).then(res => res.json()) .then(response => { for (let filename in response) { let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); if (node === null) continue; let status = response[filename]; let klass; if (status === null) { klass = 'unknown'; } else if (status === true) { klass = 'online'; node.setAttribute('data-last-connected', Date.now().toString()); } else if (node.hasAttribute('data-last-connected')) { const attr = parseInt(node.getAttribute('data-last-connected')); if (Date.now() - attr <= 5000) { klass = 'not-responding'; } else { klass = 'offline'; } } else { klass = 'offline'; } if (node.classList.contains(klass)) continue; node.classList.remove('unknown', 'online', 'offline', 'not-responding'); node.classList.add(klass); } isFetchingPing = false; }); }; setInterval(fetchPing, 2000); fetchPing(); const portSelect = document.querySelector('.nav-wrapper select'); let ports = []; const fetchSerialPorts = (begin=false) => { fetch('/serial-ports', {credentials: "same-origin"}).then(res => res.json()) .then(response => { if (ports.length === response.length) { let allEqual = true; for (let i = 0; i < response.length; i++) { if (ports[i].port !== response[i].port) { allEqual = false; break; } } if (allEqual) return; } const hasNewPort = response.length >= ports.length; ports = response; const inst = M.FormSelect.getInstance(portSelect); if (inst !== undefined) { inst.destroy(); } portSelect.innerHTML = ""; const prevSelected = getUploadPort(); for (let i = 0; i < response.length; i++) { const val = response[i]; if (val.port === prevSelected) { portSelect.innerHTML += ``; } else { portSelect.innerHTML += ``; } } M.FormSelect.init(portSelect, {}); if (!begin && hasNewPort) M.toast({html: "Discovered new serial port."}); }); }; const getUploadPort = () => { const inst = M.FormSelect.getInstance(portSelect); if (inst === undefined) { return "OTA"; } inst._setSelectedStates(); return inst.getSelectedValues()[0]; }; setInterval(fetchSerialPorts, 5000); fetchSerialPorts(true); const logsModalElem = document.getElementById("modal-logs"); document.querySelectorAll(".action-show-logs").forEach((showLogs) => { showLogs.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(logsModalElem); const log = logsModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = logsModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = logsModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/logs"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); } else { M.toast({html: `Program failed with code ${data.code}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const uploadModalElem = document.getElementById("modal-upload"); document.querySelectorAll(".action-upload").forEach((upload) => { upload.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(uploadModalElem); const log = uploadModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = uploadModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = uploadModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/run"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); } else { M.toast({html: `Program failed with code ${data.code}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const validateModalElem = document.getElementById("modal-validate"); document.querySelectorAll(".action-validate").forEach((upload) => { upload.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(validateModalElem); const log = validateModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = validateModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = validateModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/validate"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { if (data.code === 0) { M.toast({ html: `${configuration} is valid 👍`, displayLength: 5000, }); } else { M.toast({ html: `${configuration} is invalid 😕`, displayLength: 5000, }); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const compileModalElem = document.getElementById("modal-compile"); const downloadButton = compileModalElem.querySelector('.download-binary'); document.querySelectorAll(".action-compile").forEach((upload) => { upload.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(compileModalElem); const log = compileModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = compileModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; downloadButton.classList.add('disabled'); modalInstance.open(); const filenameField = compileModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/compile"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); downloadButton.classList.remove('disabled'); } else { M.toast({html: `Program failed with code ${data.code}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); downloadButton.addEventListener('click', () => { const link = document.createElement("a"); link.download = name; link.href = '/download.bin?configuration=' + encodeURIComponent(configuration); link.click(); }); const cleanMqttModalElem = document.getElementById("modal-clean-mqtt"); document.querySelectorAll(".action-clean-mqtt").forEach((btn) => { btn.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(cleanMqttModalElem); const log = cleanMqttModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = cleanMqttModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = cleanMqttModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/clean-mqtt"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const cleanModalElem = document.getElementById("modal-clean"); document.querySelectorAll(".action-clean").forEach((btn) => { btn.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(cleanModalElem); const log = cleanModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = cleanModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = cleanModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/clean"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); downloadButton.classList.remove('disabled'); } else { M.toast({html: `Program failed with code ${data.code}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const hassConfigModalElem = document.getElementById("modal-hass-config"); document.querySelectorAll(".action-hass-config").forEach((btn) => { btn.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(hassConfigModalElem); const log = hassConfigModalElem.querySelector(".log"); log.innerHTML = ""; const stopLogsButton = hassConfigModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; modalInstance.open(); const filenameField = hassConfigModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; const logSocket = new WebSocket(wsUrl + "/hass-config"); logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { const msg = data.data; log.innerHTML += colorReplace(msg); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); downloadButton.classList.remove('disabled'); } else { M.toast({html: `Program failed with code ${data.code}`}); } stopLogsButton.innerHTML = "Close"; stopped = true; } }); logSocket.addEventListener('open', () => { const msg = JSON.stringify({configuration: configuration}); logSocket.send(msg); }); logSocket.addEventListener('close', () => { if (!stopped) { M.toast({html: 'Terminated process.'}); } }); modalInstance.options.onCloseStart = () => { logSocket.close(); }; }); }); const editModalElem = document.getElementById("modal-editor"); const editorElem = editModalElem.querySelector("#editor"); const editor = ace.edit(editorElem); editor.setTheme("ace/theme/dreamweaver"); editor.session.setMode("ace/mode/yaml"); editor.session.setValue("Hello World!"); editor.session.setOption('useSoftTabs', true); editor.session.setOption('tabSize', 2); const saveButton = editModalElem.querySelector(".save-button"); const saveEditor = () => { fetch(`/edit?configuration=${configuration}`, { credentials: "same-origin", method: "POST", body: editor.getValue() }).then(res => res.text()).then(() => { M.toast({ html: `Saved ${configuration}` }); }); }; editor.commands.addCommand({ name: 'saveCommand', bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, exec: saveEditor, readOnly: false }); saveButton.addEventListener('click', saveEditor); document.querySelectorAll(".action-edit").forEach((btn) => { btn.addEventListener('click', (e) => { configuration = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(editModalElem); const filenameField = editModalElem.querySelector('.filename'); filenameField.innerHTML = configuration; fetch(`/edit?configuration=${configuration}`, {credentials: "same-origin"}) .then(res => res.text()).then(response => { editor.setValue(response, -1); }); modalInstance.open(); }); }); const modalSetupElem = document.getElementById("modal-wizard"); const setupWizardStart = document.getElementById('setup-wizard-start'); const startWizard = () => { const modalInstance = M.Modal.getInstance(modalSetupElem); modalInstance.open(); modalInstance.options.onCloseStart = () => { }; $('.stepper').activateStepper({ linearStepsNavigation: false, autoFocusInput: true, autoFormCreation: true, showFeedbackLoader: true, parallel: false }); }; setupWizardStart.addEventListener('click', startWizard);