const $ = document.querySelector.bind(document); const WAKEUP_REASON_TIMED = 0; const WAKEUP_REASON_BOOT = 1; const WAKEUP_REASON_GPIO = 2; const WAKEUP_REASON_NFC = 3; const WAKEUP_REASON_BUTTON1 = 4; const WAKEUP_REASON_BUTTON2 = 5; const WAKEUP_REASON_BUTTON3 = 6; const WAKEUP_REASON_FAILED_OTA_FW = 0xE0; const WAKEUP_REASON_FIRSTBOOT = 0xFC; const WAKEUP_REASON_NETWORK_SCAN = 0xFD; const WAKEUP_REASON_WDT_RESET = 0xFE; let tagTypes = {}; let apConfig = {}; let tagDB = {}; const previewWindows = []; const apstate = [ { state: "offline, please wait...", color: "orange", icon: "warning" }, { state: "online", color: "green", icon: "check_circle" }, { state: "flashing", color: "orange", icon: "flash_on" }, { state: "wait for reset", color: "blue", icon: "hourglass" }, { state: "AP requires reboot", color: "purple", icon: "refresh" }, { state: "failed", color: "red", icon: "error" }, { state: "coming online...", color: "orange", icon: "hourglass" }, { state: "AP without radio", color: "green", icon: "wifi_off" } ]; const runstate = [ { state: "⏹︎ stopped" }, { state: "⏸ pause" }, { state: "" }, // hide running { state: "⏳︎ init" } ]; const imageQueue = []; let isProcessing = false; let servertimediff = 0; let paintLoaded = false, paintShow = false; let cardconfig; let otamodule, flashmodule; let socket; let finishedInitialLoading = false; let getTagtypeBusy = false; const loadConfig = new Event("loadConfig"); window.addEventListener("loadConfig", function () { fetch("get_ap_config") .then(response => response.json()) .then(data => { apConfig = data; if (data.alias) { $(".logo").innerHTML = data.alias; this.document.title = data.alias; } if (data.C6 == 1 || (data.H2 && data.H2 == 1)) { var optionToRemove = $("#apcfgchid").querySelector('option[value="27"]'); if (optionToRemove) $("#apcfgchid").removeChild(optionToRemove); } if (data.hasFlasher == 1) { $('[data-target="flashtab"]').style.display = 'block'; } if (data.hasBLE == 0) { $("#apcfgble").parentNode.style.display = 'none'; } if (data.hasSubGhz == 0) { $("#apcfgsubgigchid").parentNode.style.display = 'none'; } if (data.savespace) { } if (data.apstate) { $("#apstatecolor").innerHTML = apstate[data.apstate].icon; $("#apstatecolor").style.color = apstate[data.apstate].color; $("#apstate").innerHTML = apstate[data.apstate].state; $('#dashboardStatus').innerHTML = apstate[data.apstate].state; $('#dashboardStatus').style.color = apstate[data.apstate].color; $('#dashboardStatusIcon').innerHTML = apstate[data.apstate].icon; $('#dashboardStatusIcon').style.color = apstate[data.apstate].color; } }); }); window.addEventListener("load", function () { window.dispatchEvent(loadConfig); initTabs(); fetch('content_cards.json') .then(response => response.json()) .then(data => { cardconfig = data; loadTags(0) .then(() => { finishedInitialLoading = true; connect(); }) .catch(error => showMessage('loadTags error: ' + error)); setInterval(updatecards, 1000); }) .catch(error => { console.error('Error:', error); alert("I can't load /www/content_cards.json.\r\nHave you upload it to the data partition?"); }); dropUpload(); populateTimes($('#apcnight1')); populateTimes($('#apcnight2')); document.addEventListener('DOMContentLoaded', function () { var faviconLink = document.createElement('link'); faviconLink.rel = 'icon'; faviconLink.href = 'favicon.ico'; document.head.appendChild(faviconLink); }); }); /* tabs */ let activeTab = '', previousTab = ''; function initTabs() { const tabLinks = document.querySelectorAll(".tablinks"); const tabContents = document.querySelectorAll(".tabcontent"); tabLinks.forEach(tabLink => { tabLink.addEventListener("click", function (event) { event.preventDefault(); const targetId = this.getAttribute("data-target"); const loadTabEvent = new CustomEvent('loadTab', { detail: targetId }); document.dispatchEvent(loadTabEvent); tabContents.forEach(tabContent => { tabContent.style.display = "none"; }); tabLinks.forEach(link => { link.classList.remove("active"); }); if (targetId == "logtab") document.getElementById(targetId).scrollTop = 0; document.getElementById(targetId).style.display = "block"; this.classList.add("active"); }); }); tabLinks[0].click(); }; function loadTags(pos) { return fetch("get_db?pos=" + pos) .then(response => response.json()) .then(data => { processTags(data.tags); if (data.continu && data.continu > pos) { return loadTags(data.continu); } }); } function formatUptime(seconds) { const days = Math.floor(seconds / (24 * 60 * 60)); const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60)); const minutes = Math.floor((seconds % (60 * 60)) / 60); const remainingSeconds = seconds % 60; const components = [ { value: days, label: 'd' }, { value: hours, label: 'h' }, { value: minutes, label: 'm' }, { value: remainingSeconds, label: 's' } ]; let formattedUptime = ''; components.forEach(({ value, label }) => { if (value > 0 || formattedUptime !== '') { formattedUptime += `${value}${label} `; } }); return formattedUptime.trim(); } function connect() { protocol = location.protocol == "https:" ? "wss://" : "ws://"; socket = new WebSocket(protocol + location.host + location.pathname + "ws"); socket.addEventListener("open", (event) => { showMessage("websocket connected"); }); socket.addEventListener("message", (event) => { if ($('#showdebug').checked) { showMessage(event.data); console.log(event.data); } const msg = JSON.parse(event.data); if (msg.logMsg) { showMessage(msg.logMsg, false); } if (msg.errMsg) { showMessage(msg.errMsg, true); } if (msg.tags) { processTags(msg.tags); } if (msg.sys) { let str = ""; str += `free heap: ${convertSize(msg.sys.heap)} ┇ `; if (msg.sys.psfree) { str += `free PSRAM: ${convertSize(msg.sys.psfree)} ┇ `; } str += `db size: ${convertSize(msg.sys.dbsize)} ┇ `; str += `db record count: ${msg.sys.recordcount} ┇ `; if (msg.sys.littlefsfree < 31000) { str += `filesystem FULL! ${convertSize( msg.sys.littlefsfree )} `; } else { str += `filesystem free: ${convertSize(msg.sys.littlefsfree)}`; } str += ` ┇ uptime: ${formatUptime(msg.sys.uptime)}`; $("#sysinfo").innerHTML = str; if (msg.sys.apstate) { $("#runstate").innerHTML = runstate[msg.sys.runstate].state; $("#apstatecolor").innerHTML = apstate[msg.sys.apstate].icon; $("#apstatecolor").style.color = apstate[msg.sys.apstate].color; $("#apstate").innerHTML = apstate[msg.sys.apstate].state; $('#dashboardStatus').innerHTML = apstate[msg.sys.apstate].state; $('#dashboardStatus').style.color = apstate[msg.sys.apstate].color; $('#dashboardStatusIcon').innerHTML = apstate[msg.sys.apstate].icon; $('#dashboardStatusIcon').style.color = apstate[msg.sys.apstate].color; } servertimediff = (Date.now() / 1000) - msg.sys.currtime; } if (msg.apitem) { populateAPCard(msg.apitem); } if (msg.console) { if (activeTab == 'flashtab' && flashmodule && typeof (flashmodule.print) === "function") { let color = (msg.color ? msg.color : "#c0c0c0"); if (msg.console.startsWith("Fail") || msg.console.startsWith("Err")) { color = "red"; } flashmodule.print(msg.console, color); } else if (otamodule && typeof (otamodule.print) === "function") { let color = "#c0c0c0"; if (msg.console.startsWith("Fail") || msg.console.startsWith("Err")) { color = "red"; } otamodule.print(msg.console, color); } } }); socket.addEventListener("close", (event) => { showMessage(`websocket closed ${event.code}`); setTimeout(connect, 5000); }); } function convertSize(bytes) { if (bytes >= 1073741824) { bytes = (bytes / 1073741824).toFixed(2) + " GB"; } else if (bytes >= 1048576) { bytes = (bytes / 1048576).toFixed(2) + " MB"; } else if (bytes >= 1024) { bytes = (bytes / 1024).toFixed(2) + " kB"; } else if (bytes > 1) { bytes = bytes + " bytes"; } else if (bytes == 1) { bytes = bytes + " byte"; } else { bytes = "0 bytes"; } return bytes; } function processTags(tagArray) { for (const element of tagArray) { const tagmac = element.mac; tagDB[tagmac] = element; let div = $('#tag' + tagmac); if (div == null) { div = $('#tagtemplate').cloneNode(true); div.setAttribute('id', 'tag' + tagmac); div.dataset.mac = tagmac; div.dataset.hwtype = -1; $('#taglist').appendChild(div); } div.style.display = 'block'; if (element.contentMode == 255) { div.remove(); showMessage(tagmac + " removed by remote AP"); continue; } if (element.isexternal) { $('#tag' + tagmac + ' .mac').innerHTML = tagmac + " via ext AP"; } else { $('#tag' + tagmac + ' .mac').innerHTML = tagmac; } let alias = element.alias; if (!alias) { alias = tagmac.replace(/^0{1,4}/, ''); if (alias.match(/^4467/)) { let macdigit = Number.parseInt(alias.substr(4, 2), 16) & 0x1f; let model = String.fromCharCode(macdigit + 65); if (model >= 'A' && model <= 'Z') { macdigit = Number.parseInt(alias.substr(6, 2), 16) & 0x1f; model += String.fromCharCode(macdigit + 65); alias = model + alias.substr(8, 8) + 'x' } } } if ($('#tag' + tagmac + ' .alias').innerHTML != alias) { $('#tag' + tagmac + ' .alias').innerHTML = alias; } let contentDefObj = getContentDefById(element.contentMode); if (contentDefObj) $('#tag' + tagmac + ' .contentmode').innerHTML = contentDefObj.name; if (element.RSSI) { div.dataset.hwtype = element.hwType; (async () => { const localTagmac = tagmac; const data = await getTagtype(element.hwType); div.dataset.usetemplate = data.usetemplate; if (data.usetemplate != 0) { const template = await getTagtype(data.usetemplate); } $('#tag' + localTagmac + ' .model').innerHTML = data.name; $('#tag' + localTagmac + ' .resolution').innerHTML = data.width + "x" + data.height; if (element.ver != 0 && element.ver != 1) { div.dataset.ver = element.ver; $('#tag' + localTagmac + ' .resolution').innerHTML += ` fw:${element.ver} 0x${element.ver.toString(16)}`; } if (!apConfig.preview || element.contentMode == 20) { $('#tag' + tagmac + ' .tagimg').style.display = 'none' } else if (div.dataset.hash != element.hash && div.dataset.hwtype > -1) { let cachetag = element.hash; if (element.hash != '00000000000000000000000000000000') { if (element.isexternal && element.contentMode == 12) { loadImage(tagmac, 'http://' + tagDB[tagmac].apip + '/current/' + tagmac + '.raw?' + cachetag); } else { loadImage(tagmac, 'current/' + tagmac + '.raw?' + cachetag); } } else { $('#tag' + tagmac + ' .tagimg').style.display = 'none' } div.dataset.hash = element.hash; } })(); let statusline = ""; if (element.RSSI != 100) { if (element.ch > 0) statusline += `CH ${element.ch}, `; statusline += `RSSI ${element.RSSI}, LQI ${element.LQI}`; } else { statusline = "AP"; } if (element.batteryMv != 0 && element.batteryMv != 1337) { statusline += ", " + (element.batteryMv == 2600 ? "≥" : "") + (element.batteryMv / 1000) + "V"; } $('#tag' + tagmac + ' .received').innerHTML = statusline; $('#tag' + tagmac + ' .received').style.opacity = "1"; } else { $('#tag' + tagmac + ' .model').innerHTML = "waiting for hardware type"; $('#tag' + tagmac + ' .received').style.opacity = "0"; $('#tag' + tagmac + ' .resolution').innerHTML = ""; } if (element.nextupdate > 1672531200 && element.nextupdate != 3216153600) { const date = new Date(element.nextupdate * 1000); const options = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; $('#tag' + tagmac + ' .nextupdate').innerHTML = "next update" + date.toLocaleString('nl-NL', options); } else { $('#tag' + tagmac + ' .nextupdate').innerHTML = ""; } if (element.nextupdate < (Date.now() / 1000) - servertimediff) { $('#tag' + tagmac + ' .waitingicon').style.display = 'inline-block'; } else { $('#tag' + tagmac + ' .waitingicon').style.display = 'none'; } if (element.nextcheckin > 1672531200) { div.dataset.nextcheckin = element.nextcheckin; } else { div.dataset.nextcheckin = element.lastseen + 60; } div.style.opacity = '1'; $('#tag' + tagmac + ' .lastseen').style.color = "black"; div.classList.remove("tagpending"); div.dataset.lastseen = element.lastseen; div.dataset.wakeupreason = element.wakeupReason; div.dataset.nextupdate = element.nextupdate; div.dataset.channel = element.ch; div.dataset.isexternal = element.isexternal; $('#tag' + tagmac + ' .warningicon').style.display = 'none'; $('#tag' + tagmac).style.background = "#ffffff"; if (element.contentMode == 12 || element.nextcheckin == 3216153600) $('#tag' + tagmac).style.background = "#e4e4e0"; switch (parseInt(element.wakeupReason)) { case WAKEUP_REASON_TIMED: break; case WAKEUP_REASON_BOOT: case WAKEUP_REASON_FIRSTBOOT: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "First boot" $('#tag' + tagmac).style.background = "#b0d0b0"; break; case WAKEUP_REASON_GPIO: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "GPIO wakeup" $('#tag' + tagmac).style.background = "#c8f1bb"; break; case WAKEUP_REASON_BUTTON1: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "Button 1 pressed" $('#tag' + tagmac).style.background = "#c8f1bb"; break; case WAKEUP_REASON_BUTTON2: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "Button 2 pressed" $('#tag' + tagmac).style.background = "#c8f1bb"; break; case WAKEUP_REASON_BUTTON3: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "Button 3 pressed" $('#tag' + tagmac).style.background = "#c8f1bb"; break; case WAKEUP_REASON_NFC: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "NFC wakeup" $('#tag' + tagmac).style.background = "#c8f1bb"; break; case WAKEUP_REASON_NETWORK_SCAN: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "Network scan" $('#tag' + tagmac).style.background = "#c0c0d0"; break; case WAKEUP_REASON_WDT_RESET: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "Watchdog reset!" $('#tag' + tagmac).style.background = "#d0a0a0"; break; case WAKEUP_REASON_FAILED_OTA_FW: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "Firmware update rejected!" $('#tag' + tagmac).style.background = "#f0a0a0"; break; } $('#tag' + tagmac + ' .pendingicon').style.display = (element.pending ? 'inline-block' : 'none'); $('#tag' + tagmac + ' .pendingicon').innerHTML = element.pending; div.classList.add("tagflash"); (function (tagmac) { setTimeout(function () { $('#tag' + tagmac).classList.remove("tagflash"); }, 1400); })(tagmac); if (element.pending) div.classList.add("tagpending"); previewWindows.forEach((previewWindow, index) => { if (previewWindow && !previewWindow.closed) { if (previewWindow.mac === tagmac) { previewWindow.updateMessage(element); } } else { previewWindows.splice(index, 1); } }); } GroupSortFilter(); } function updatecards() { if (servertimediff > 1000000000) servertimediff = 0; let tagcount = 0; let pendingcount = 0; let timeoutcount = 0; let lowbattcount = 0; $('#taglist').querySelectorAll('[data-mac]').forEach(item => { let tagmac = item.dataset.mac; tagcount++; if (tagDB[tagmac].batteryMv < 2400 && tagDB[tagmac].batteryMv != 0 && tagDB[tagmac].batteryMv != 1337) lowbattcount++; if (item.dataset.lastseen && item.dataset.lastseen > (Date.now() / 1000) - servertimediff - 30 * 24 * 3600 * 60) { let idletime = (Date.now() / 1000) - servertimediff - item.dataset.lastseen; $('#tag' + tagmac + ' .lastseen').innerHTML = "last seen" + displayTime(Math.floor(idletime)) + " ago"; if ((Date.now() / 1000) - servertimediff - apConfig.maxsleep * 60 - 300 > item.dataset.nextcheckin) { $('#tag' + tagmac + ' .warningicon').style.display = 'inline-block'; $('#tag' + tagmac).classList.remove("tagpending") $('#tag' + tagmac).style.background = '#e0e0a0'; timeoutcount++; } else { if (tagDB[tagmac].pending) pendingcount++; } if (idletime > 24 * 3600) { $('#tag' + tagmac).style.opacity = '.5'; $('#tag' + tagmac + ' .lastseen').style.color = "red"; } } else { if ($('#tag' + tagmac + ' .lastseen')) { $('#tag' + tagmac + ' .lastseen').innerHTML = "" } else { console.log(tagmac + " not found") } } if (item.dataset.nextcheckin == 3216153600) { $('#tag' + tagmac + ' .nextcheckin').innerHTML = "In deep sleep"; } else if (item.dataset.nextcheckin > 1672531200 && parseInt(item.dataset.wakeupreason) == 0) { let nextcheckin = item.dataset.nextcheckin - ((Date.now() / 1000) - servertimediff); $('#tag' + tagmac + ' .nextcheckin').innerHTML = "expected checkin" + displayTime(Math.floor(nextcheckin)); } else { // $('#tag' + tagmac + ' .nextcheckin').innerHTML = ""; } if (item.dataset.nextupdate < (Date.now() / 1000) - servertimediff) { $('#tag' + tagmac + ' .waitingicon').style.display = 'inline-block'; } else { $('#tag' + tagmac + ' .waitingicon').style.display = 'none'; } }) $('#dashboardTagCount').innerHTML = tagcount; $('#dashboardPending').innerHTML = pendingcount; $('#dashboardLowBatt').innerHTML = lowbattcount; $('#dashboardTimeout').innerHTML = timeoutcount; } $('#clearlog').addEventListener("click", (event) => { $('#messages').innerHTML = ''; }); document.querySelectorAll('.closebtn').forEach(button => { button.addEventListener('click', (event) => { event.target.parentNode.style.display = 'none'; $('#advancedoptions').style.height = '0px'; }); }); document.querySelectorAll('.closebtn2').forEach(button => { button.addEventListener('click', (event) => { event.target.parentNode.close(); $('#advancedoptions').style.height = '0px'; }); }); //clicking on a tag: load config dialog for tag $('#taglist').addEventListener("click", (event) => { let currentElement = event.target; while (currentElement !== $('#taglist')) { if (currentElement.classList.contains("tagcard")) { break; } currentElement = currentElement.parentNode; } if (!currentElement.classList.contains("tagcard")) { return; } const mac = currentElement.dataset.mac; loadContentCard(mac); }) function loadContentCard(mac) { $('#cfgmac').innerHTML = mac; $('#cfgmac').dataset.mac = mac; fetch("get_db?mac=" + mac) .then(response => response.json()) .then(data => { const tagdata = data.tags[0]; $('#cfgalias').value = tagdata.alias; $('#cfgmore').style.display = "none"; if (populateSelectTag(tagdata.hwType, tagdata.capabilities)) { $('#cfgcontent').parentNode.style.display = "flex"; $('#cfgcontent').value = tagdata.contentMode; $('#cfgcontent').dataset.json = tagdata.modecfgjson; contentselected(); if (tagdata.contentMode != 12) $('#cfgmore').style.display = 'block'; } else { $('#customoptions').innerHTML = ""; $('#cfgcontent').parentNode.style.display = "none"; } $('#cfgrotate').value = tagdata.rotate; $('#cfglut').value = tagdata.lut; $('#cfginvert').value = tagdata.invert; $('#cfgmore').innerHTML = '▼'; $('#cfgmac').dataset.ch = tagdata.ch; $('#configbox').showModal(); }) } let typedString = ''; document.addEventListener('keypress', (event) => { const keyPressed = event.key; if (keyPressed.length === 1) { typedString += keyPressed; } else if (keyPressed === 'Enter') { typedString = ('0000' + typedString).slice(-16); const hexRegExp = /^[0-9A-Fa-f]{16}$/; const isMac = typedString.match(hexRegExp); if (isMac) { console.log('scanned ' + typedString); loadContentCard(typedString); } typedString = ''; } }); $('#cfgmore').onclick = function () { $('#cfgmore').innerHTML = $('#advancedoptions').style.height == '0px' ? '▲' : '▼'; $('#advancedoptions').style.height = $('#advancedoptions').style.height == '0px' ? $('#advancedoptions').scrollHeight + 'px' : '0px'; }; $('#cfgsave').onclick = function () { let contentMode = $('#cfgcontent').value; let contentDef = getContentDefById(contentMode); let extraoptions = contentDef?.param ?? null; let obj = {}; let formData = new FormData(); formData.append("mac", $('#cfgmac').dataset.mac); formData.append("alias", $('#cfgalias').value); if (contentMode) { extraoptions?.forEach(element => { if (document.getElementById('opt' + element.key)) { obj[element.key] = document.getElementById('opt' + element.key).value; } }); formData.append("contentmode", contentMode); formData.append("modecfgjson", JSON.stringify(obj)); } else { formData.append("contentmode", "0"); formData.append("modecfgjson", String()); } formData.append("rotate", $('#cfgrotate').value); formData.append("lut", $('#cfglut').value); formData.append("invert", $('#cfginvert').value); fetch("save_cfg", { method: "POST", body: formData }) .then(response => response.text()) .then(data => showMessage(data)) .catch(error => showMessage('Error: ' + error, true)); $('#advancedoptions').style.height = '0px'; $('#configbox').close(); backupTagDB(); } function sendCmd(mac, cmd) { let formData = new FormData(); formData.append("mac", mac); formData.append("cmd", cmd); fetch("tag_cmd", { method: "POST", body: formData }) .then(response => response.text()) .then(data => { let div = $('#tag' + mac); if (cmd == "del") div.remove(); showMessage(data); }) .catch(error => showMessage('Error: ' + error, true)); $('#advancedoptions').style.height = '0px'; $('#configbox').close(); } $('#cfgdelete').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "del"); } $('#cfgclrpending').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "clear"); } $('#cfgrefresh').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "refresh"); } $('#cfgtagreboot').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "reboot"); } $('#cfgscan').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "scan"); } $('#cfgdeepsleep').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "deepsleep"); } $('#cfgreset').onclick = function () { sendCmd($('#cfgmac').dataset.mac, "reset"); } $('#cfgautoupdate').onclick = async function () { let obj = {}; let formData = new FormData(); var mac = $('#cfgmac').dataset.mac; formData.append("mac", mac); formData.append("alias", $('#cfgalias').value); var repo = apConfig.repo || 'OpenEPaperLink/OpenEPaperLink'; var infourl = "https://raw.githubusercontent.com/" + repo + "/master/binaries/Tag/tagotaversions.json"; var info = ""; await fetch(infourl, { method: 'GET' }).then(await function (response) { return response.json(); }).then(await function (json) { info = json; }); var tagtype = ("0" + (Number($('#tag' + mac).dataset.hwtype).toString(16))).slice(-2).toUpperCase(); var name = info[0][tagtype]["type"]; if (name == "") { alert("Tag id not known"); return false; } var version = info[0][tagtype]["version"]; var md5 = info[0][tagtype]["md5"]; if (name.substr(0, 6) == "chroma") { var variation = (Number.parseInt(mac.substr(4, 2), 16) >> 5).toString(); if (variation != '0') { var name = info[0][tagtype]["type_" + variation]; version = info[0][tagtype]['version_' + variation]; md5 = info[0][tagtype]['md5_' + variation]; } } var currentversion = $('#tag' + mac).dataset.ver | 0; if (confirm(`Current version: ${currentversion} 0x${currentversion.toString(16)}\nPending version: ${parseInt(version, 16)} 0x${parseInt(version, 16).toString(16)}\n\nNOTE: Every OTA update comes with a risk of bricking the tag, if it is bricked, it only can be recoverd with a tag flasher. Please only update if you need the new features.\n\nPress Cancel if you want to get out of here, or press OK if you want to proceed with the update.`)) { var fullFilename = name + "_" + version + ".bin"; var filepath = "/" + fullFilename; var binurl = "https://raw.githubusercontent.com/" + repo + "/master/binaries/Tag/" + fullFilename; var url = "check_file?path=" + encodeURIComponent(filepath); var response = await fetch(url); if (response.ok) { var data = await response.json(); if (data.filesize == 0 || data.md5 != md5) { try { var response = await fetch(binurl); var fileContent = await response.blob(); var formData2 = new FormData(); formData2.append('path', filepath); formData2.append('file', fileContent, fullFilename); var uploadResponse = await fetch('littlefs_put', { method: 'POST', body: formData2 }); if (!uploadResponse.ok) { showMessage('Error: auto update failed to upload', true); } } catch (error) { showMessage('Error: ' + error, true); } } } else showMessage('Error: auto update failed', true); var response = await fetch(url); if (response.ok) { var data = await response.json(); if (data.filesize == 0 || data.md5 != md5) { showMessage('Error: auto update failed to download. File is empty or md5 check fails', true); } //sucess else obj["filename"] = filepath; } else showMessage('Error: auto update failed', true); formData.append("contentmode", 5); formData.append("modecfgjson", JSON.stringify(obj)); fetch("save_cfg", { method: "POST", body: formData }) .then(response => response.text()) .then(data => showMessage(data)) .catch(error => showMessage('Error: ' + error, true)); } $('#configbox').close(); } $('#rebootbutton').onclick = function (event) { event.preventDefault(); if (!confirm('Reboot AP now?')) return; socket.close(); fetch("reboot", { method: "POST" }); alert('Rebooted. Webpage will reload.'); location.reload() } $('#configbox').addEventListener('click', (event) => { if (event.target.nodeName === 'DIALOG') { $('#configbox').close(); } }); document.addEventListener("loadTab", function (event) { activeTab = event.detail; switch (event.detail) { case 'configtab': case 'aptab': fetch("get_ap_config") .then(response => response.json()) .then(data => { if (data && 'alias' in data) { apConfig = data; $('#apcfgalias').value = data.alias; $('#apcfgchid').value = data.channel; $('#apcfgsubgigchid').value = data.subghzchannel; $('#apcfgble').value = data.ble; $("#apcfgledbrightness").value = data.led; $("#apcfgtftbrightness").value = data.tft; $("#apcfglanguage").value = data.language; $("#apclatency").value = data.maxsleep; $("#apcpreventsleep").value = data.stopsleep; $("#apcpreview").value = data.preview; $("#apcnightlyreboot").value = data.nightlyreboot; $("#apclock").value = data.lock; $("#apcwifipower").value = data.wifipower; $("#apctimezone").value = data.timezone; $("#apcnight1").value = data.sleeptime1; $("#apcnight2").value = data.sleeptime2; $("#apcdiscovery").value = data.discovery; $("#apcshowtimestamp").value = data.showtimestamp; } }) $('#apcfgmsg').innerHTML = ''; break; case 'updatetab': $('#updateconsole').innerHTML = ''; loadOTA(); break; case 'flashtab': // $('#flashconsole').innerHTML = ''; loadFlash(); break; } if (previousTab == 'flashtab' && activeTab != 'flashtab' && flashmodule && typeof (flashmodule.wsCmd) === "function") { flashmodule.wsCmd(flashmodule.WEBFLASH_BLUR); } previousTab = activeTab; }); $('#apcfgsave').onclick = function () { let formData = new FormData(); formData.append("alias", $('#apcfgalias').value); formData.append("channel", $('#apcfgchid').value); formData.append("subghzchannel", $('#apcfgsubgigchid').value); formData.append('ble', $('#apcfgble').value); formData.append('led', $('#apcfgledbrightness').value); formData.append('tft', $('#apcfgtftbrightness').value); formData.append('language', $('#apcfglanguage').value); formData.append('maxsleep', $('#apclatency').value); formData.append('stopsleep', $('#apcpreventsleep').value); formData.append('preview', $('#apcpreview').value); formData.append('nightlyreboot', $('#apcnightlyreboot').value); formData.append('lock', $('#apclock').value); formData.append('wifipower', $('#apcwifipower').value); formData.append('timezone', $('#apctimezone').value); formData.append('sleeptime1', $('#apcnight1').value); formData.append('sleeptime2', $('#apcnight2').value); formData.append('discovery', $('#apcdiscovery').value); formData.append('showtimestamp', $('#apcshowtimestamp').value); fetch("save_apcfg", { method: "POST", body: formData }) .then(response => response.text()) .then(data => { showMessage(data); window.dispatchEvent(loadConfig); $('#apcfgmsg').innerHTML = 'OK, Saved'; }) .catch(error => showMessage('Error: ' + error, true)); } $('#uploadButton').onclick = function () { const file = $('#fileInput')?.files[0]; if (file) { const formData = new FormData(); formData.append('file', file); fetch('restore_db', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return response.text(); }) .then(data => { alert('TagDB restored. Webpage will reload.'); location.reload(); }) .catch(error => { alert('Error uploading file: ' + error); }); } else { alert('No file selected'); } } $('#restoreFromLocal').onclick = function () { var tagDBrestore = localStorage.getItem('tagDB'); if (tagDBrestore) { tagDBobj = JSON.parse(tagDBrestore); var tagResult = []; for (var key in tagDBobj) { if (tagDBobj.hasOwnProperty(key)) { tagResult.push([tagDBobj[key]]); } } const blob = new Blob([JSON.stringify(tagResult, null, '\t')], { type: 'application/json' }); const formData = new FormData(); formData.append('file', blob, 'tagResult.json'); fetch('restore_db', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return response.text(); }) .then(data => { alert('TagDB restored. Webpage will reload.'); location.reload(); }) .catch(error => { alert('Error uploading file: ' + error); }); } else { alert('No data found in localStorage'); } } async function loadOTA() { otamodule = await import('./ota.js?v=' + Date.now()); otamodule.initUpdate(); } async function loadFlash() { flashmodule = await import('./flash.js?v=' + Date.now()); flashmodule.init(); } $('#paintbutton').onclick = function () { if (paintShow) { paintShow = false; $('#cfgsave').parentNode.style.display = 'block'; contentselected(); } else { paintShow = true; $('#cfgsave').parentNode.style.display = 'none'; $('#customoptions').innerHTML = "
"; const mac = $('#cfgmac').dataset.mac const hwtype = $('#tag' + mac).dataset.hwtype; const [width, height] = [tagTypes[hwtype].width, tagTypes[hwtype].height] || [0, 0]; if (paintLoaded) { startPainter(mac, width, height, tagTypes[hwtype]); } else { loadScript('painter.js', function () { startPainter(mac, width, height, tagTypes[hwtype]); }); } } } function loadScript(url, callback) { let script = document.createElement('script'); script.src = url; script.onload = function () { if (callback) { callback(); } }; document.head.appendChild(script); } function contentselected() { let contentMode = $('#cfgcontent').value; $('#customoptions').innerHTML = ""; let obj = {}; if ($('#cfgcontent').dataset.json && ($('#cfgcontent').dataset.json != "null")) { obj = JSON.parse($('#cfgcontent').dataset.json); } $('#paintbutton').style.display = 'none'; if (contentMode) { let contentDef = getContentDefById(contentMode); if (contentDef) { $('#customoptions').innerHTML = "" + contentDef?.desc + "
" } $('#paintbutton').style.display = (contentMode == 22 || contentMode == 23 ? 'inline-block' : 'none'); let extraoptions = contentDef?.param ?? null; extraoptions?.forEach(element => { let label = document.createElement("label"); label.innerHTML = element.name; label.setAttribute("for", 'opt' + element.key); if (element.desc) { label.style.cursor = 'help'; label.title = element.desc; } let input = document.createElement("input"); switch (element.type) { case 'text': input.type = "text"; break; case 'int': input.type = "number"; break; case 'ro': input.type = "text"; input.disabled = true; break; case 'jpgfile': case 'binfile': case 'jsonfile': input = document.createElement("select"); fetch('edit?list=%2F&recursive=1') .then(response => response.json()) .then(data => { let files = data.filter(item => item.type === "file" && item.name.endsWith(".jpg")); if (element.type == 'binfile') files = data.filter(item => item.type === "file" && item.name.endsWith(".bin")); if (element.type == 'jsonfile') files = data.filter(item => item.type === "file" && item.name.endsWith(".json")); const optionElement = document.createElement("option"); optionElement.value = ""; optionElement.text = ""; input.appendChild(optionElement); files.forEach(item => { const optionElement = document.createElement("option"); optionElement.value = item.name; optionElement.text = item.name; if (obj[element.key] === item.name) optionElement.selected = true; input.appendChild(optionElement); }) }) .catch(error => { console.error("Error fetching JSON data:", error); }); break; case 'select': case 'chanselect': input = document.createElement("select"); let options; if (element.type == 'chanselect') { if ($('#cfgmac').dataset.ch < 100) { options = element.chans; } else { options = element.subchans; } } else { options = element.options } for (const key in options) { const optionElement = document.createElement("option"); optionElement.value = key; optionElement.text = options[key]; if (options[key].substring(0, 1) == "-") { optionElement.text = options[key].substring(1); optionElement.selected = true; } else { optionElement.selected = false; } input.appendChild(optionElement); } break; case 'geoselect': input.type = "text"; input.classList.add("geoselect"); input.setAttribute("autocomplete", "off"); break; } input.id = 'opt' + element.key; input.title = element.desc; if (obj[element.key]) input.value = obj[element.key]; let p = document.createElement("p"); p.appendChild(label); p.appendChild(input); if (element.type == 'geoselect') { input.addEventListener('input', debounce(searchLocations, 300)); const resultsContainer = document.createElement('div'); resultsContainer.id = 'georesults'; p.appendChild(resultsContainer); } $('#customoptions').appendChild(p); }); } paintShow = false; $('#cfgsave').parentNode.style.display = 'block'; } function populateSelectTag(hwtype, capabilities) { let selectTag = $("#cfgcontent"); selectTag.innerHTML = ""; let optionsAdded = false; let option; cardconfig.forEach(item => { const capcheck = item.capabilities ?? 0; if (tagTypes[hwtype].contentids?.includes(item.id) && (capabilities & capcheck || capcheck == 0) && (apConfig.savespace == 0 || !item.properties?.includes("savespace"))) { option = document.createElement("option"); option.value = item.id; option.text = item.name; selectTag.appendChild(option); optionsAdded = true; } }); let rotateTag = $("#cfgrotate"); rotateTag.innerHTML = ""; for (let i = 0; i < 4; i++) { if (i == 0 || tagTypes[hwtype].width == tagTypes[hwtype].height || (i == 2)) { option = document.createElement("option"); option.value = i; option.text = (i * 90) + " degrees"; rotateTag.appendChild(option); } } let lutTag = $("#cfglut"); lutTag.innerHTML = ""; option = document.createElement("option"); option.value = "0"; if (tagTypes[hwtype].shortlut == 0) { option.text = "Always full refresh"; } else { option.text = "auto"; } lutTag.appendChild(option); if (tagTypes[hwtype].shortlut > 0) { option = document.createElement("option"); option.value = "1"; option.text = "Always full refresh"; lutTag.appendChild(option); option = document.createElement("option"); option.value = "2"; option.text = "Fast (no reds)"; lutTag.appendChild(option); option = document.createElement("option"); option.value = "3"; option.text = "Fastest (ghosting!)"; lutTag.appendChild(option); } return optionsAdded; } function getContentDefById(id) { if (id == null) return null; const obj = cardconfig.find(item => item.id == id); return obj || null; } function showMessage(message, iserr) { const messages = $('#messages'); const date = new Date(); const time = date.toLocaleTimeString('nl-NL', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); if (message.startsWith('{')) { messages.insertAdjacentHTML("afterbegin", '