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_FIRSTBOOT = 0xFC; const WAKEUP_REASON_NETWORK_SCAN = 0xFD; const WAKEUP_REASON_WDT_RESET = 0xFE; let tagTypes = {}; const apstate = [ { state: "offline", color: "red" }, { state: "online", color: "green" }, { state: "flashing", color: "orange" }, { state: "wait for reset", color: "blue" }, { state: "requires power cycle", color: "purple" }, { state: "failed", color: "red" }, { state: "coming online", color: "yellow" } ]; 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; let socket; let finishedInitialLoading = false; let getTagtypeBusy = false; window.addEventListener("load", function () { fetch('/content_cards.json') .then(response => response.json()) .then(data => { cardconfig = data; loadTags(0); connect(); 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?"); }); fetch("/get_ap_config") .then(response => response.json()) .then(data => { if (data.alias) { $(".logo").innerHTML = data.alias; this.document.title = data.alias; } }); dropUpload(); populateTimes($('#apcnight1')); populateTimes($('#apcnight2')); }); function loadTags(pos) { fetch("/get_db?pos=" + pos) .then(response => response.json()) .then(data => { processTags(data.tags); if (data.continu && data.continu > pos) loadTags(data.continu); finishedInitialLoading = true; }) //.catch(error => showMessage('loadTags error: ' + error)); } function connect() { socket = new WebSocket("ws://" + location.host + "/ws"); socket.addEventListener("open", (event) => { showMessage("websocket connected"); }); socket.addEventListener("message", (event) => { // 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 filesystem = 'filesystem free: ' + convertSize(msg.sys.littlefsfree); if (msg.sys.littlefsfree < 31000) { filesystem = 'filesystem FULL! ' + convertSize(msg.sys.littlefsfree) + ''; } $('#sysinfo').innerHTML = 'free heap: ' + convertSize(msg.sys.heap) + ' ┇ db size: ' + convertSize(msg.sys.dbsize) + ' ┇ db record count: ' + msg.sys.recordcount + ' ┇ ' + filesystem; if (msg.sys.apstate) { $("#apstatecolor").style.color = apstate[msg.sys.apstate].color; $("#apstate").innerHTML = apstate[msg.sys.apstate].state; $("#runstate").innerHTML = runstate[msg.sys.runstate].state; if (msg.sys.temp) $("#temp").innerHTML = msg.sys.temp.toFixed(1) + '°C'; } servertimediff = (Date.now() / 1000) - msg.sys.currtime; } if (msg.apitem) { let row = $("#aptable").insertRow(); row.insertCell(0).innerHTML = "" + msg.apitem.ip + ""; row.insertCell(1).innerHTML = msg.apitem.alias; row.insertCell(2).innerHTML = msg.apitem.count; row.insertCell(3).innerHTML = msg.apitem.channel; row.insertCell(4).innerHTML = msg.apitem.version; } if (msg.console) { 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; 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 ($('#tag' + tagmac + ' .alias').innerHTML != alias) { $('#tag' + tagmac + ' .alias').innerHTML = alias; //GroupSortFilter(); } 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); $('#tag' + localTagmac + ' .model').innerHTML = data.name; })(); 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"; } if (element.ver != 0 && element.ver != 1) { $('#tag' + tagmac + ' .received').title = `fw: ${element.ver}`; } else { $('#tag' + tagmac + ' .received').title = ""; } $('#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"; } if (element.contentMode == 20) { $('#tag' + tagmac + ' .tagimg').style.display = 'none'; } else if (div.dataset.hash != element.hash && div.dataset.hwtype > -1 && (!element.isexternal || element.contentMode != 12)) { loadImage(tagmac, '/current/' + tagmac + '.raw?' + element.hash); div.dataset.hash = element.hash; } if (element.isexternal && element.contentMode == 12) $('#tag' + tagmac + ' .tagimg').style.display = 'none'; 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.nextcheckin > 1672531200) { div.dataset.nextcheckin = element.nextcheckin; } else { div.dataset.nextcheckin = element.lastseen + 1800; } 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) $('#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_NFC: $('#tag' + tagmac + ' .nextcheckin').innerHTML = "NFC wakeup" 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; } $('#tag' + tagmac + ' .pendingicon').style.display = (element.pending ? 'inline-block' : 'none'); div.classList.add("tagflash"); (function (tagmac) { setTimeout(function () { $('#tag' + tagmac).classList.remove("tagflash"); }, 1400); })(tagmac); if (element.pending) div.classList.add("tagpending"); } GroupSortFilter(); } function updatecards() { if (servertimediff > 1000000000) servertimediff = 0; $('#taglist').querySelectorAll('[data-mac]').forEach(item => { let tagmac = item.dataset.mac; 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 - 600 > item.dataset.nextcheckin) { $('#tag' + tagmac + ' .warningicon').style.display = 'inline-block'; $('#tag' + tagmac).classList.remove("tagpending") $('#tag' + tagmac).style.background = '#e0e0a0'; } 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 > 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 = ""; } }) } $('#clearlog').onclick = function () { $('#messages').innerHTML = ''; } document.querySelectorAll('.closebtn').forEach(button => { button.addEventListener('click', (event) => { event.target.parentNode.style.display = 'none'; $('#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; $('#cfgmore').innerHTML = '▼'; $('#configbox').style.display = 'block'; }) } 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 ($('#opt' + element.key)) { obj[element.key] = $('#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); fetch("/save_cfg", { method: "POST", body: formData }) .then(response => response.text()) .then(data => showMessage(data)) .catch(error => showMessage('Error: ' + error)); $('#advancedoptions').style.height = '0px'; $('#configbox').style.display = 'none'; } 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)); $('#advancedoptions').style.height = '0px'; $('#configbox').style.display = 'none'; } $('#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"); } $('#rebootbutton').onclick = function () { showMessage("rebooting AP....", true); fetch("/reboot", { method: "POST" }); socket.close(); } $('#apconfigbutton').onclick = function () { let table = document.getElementById("aptable"); const rowCount = table.rows.length; for (let i = rowCount - 1; i > 0; i--) { table.deleteRow(i); } fetch("/get_ap_config") .then(response => response.json()) .then(data => { $('#apcfgalias').value = data.alias; $('#apcfgchid').value = data.channel; $("#apcfgledbrightness").value = data.led; $("#apcfglanguage").value = data.language; $("#apclatency").value = data.maxsleep; $("#apcpreventsleep").value = data.stopsleep; $("#apcpreview").value = data.preview; $("#apcwifipower").value = data.wifipower; $("#apctimezone").value = data.timezone; $("#apcnight1").value = data.sleeptime1; $("#apcnight2").value = data.sleeptime2; }) $('#apconfigbox').style.display = 'block' } $('#apcfgsave').onclick = function () { let formData = new FormData(); formData.append("alias", $('#apcfgalias').value); formData.append("channel", $('#apcfgchid').value); formData.append('led', $('#apcfgledbrightness').value); formData.append('language', $('#apcfglanguage').value); formData.append('maxsleep', $('#apclatency').value); formData.append('stopsleep', $('#apcpreventsleep').value); formData.append('preview', $('#apcpreview').value); formData.append('wifipower', $('#apcwifipower').value); formData.append('timezone', $('#apctimezone').value); formData.append('sleeptime1', $('#apcnight1').value); formData.append('sleeptime2', $('#apcnight2').value); fetch("/save_apcfg", { method: "POST", body: formData }) .then(response => response.text()) .then(data => showMessage(data)) .catch(error => showMessage('Error: ' + error)); $(".logo").innerHTML = $('#apcfgalias').value; $('#apconfigbox').style.display = 'none'; } $('#updatebutton').onclick = function () { $('#apconfigbox').style.display = 'none'; $('#apupdatebox').style.display = 'block'; loadOTA(); } async function loadOTA() { otamodule = await import('./ota.js?v=' + Date.now()); otamodule.initUpdate(); } $('#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); } else { loadScript('painter.js', function () { startPainter(mac, width, height); }); } } } 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 == 0 ? '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 'select': input = document.createElement("select"); for (const key in element.options) { const optionElement = document.createElement("option"); optionElement.value = key; optionElement.text = element.options[key]; if (element.options[key].substring(0, 1) == "-") { optionElement.text = element.options[key].substring(1); optionElement.selected = true; } else { optionElement.selected = false; } input.appendChild(optionElement); } 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); $('#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; const hwtypeArray = item.hwtype ?? []; if ((hwtypeArray.includes(hwtype) || tagTypes[hwtype].contentids.includes(item.id)) && (capabilities & capcheck || capcheck == 0)) { 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"; option.text = "auto"; lutTag.appendChild(option); if (hwtype != 240) { option = document.createElement("option"); option.value = "1"; option.text = "Always full refresh"; 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 (iserr) { messages.insertAdjacentHTML("afterbegin", '
  • ' + htmlEncode(time + ' ' + message) + '
  • '); } else { messages.insertAdjacentHTML("afterbegin", '
  • ' + htmlEncode(time + ' ' + message) + '
  • '); } } function htmlEncode(input) { const textArea = document.createElement("textarea"); textArea.innerText = input; return textArea.innerHTML.split("
    ").join("\n"); } function loadImage(id, imageSrc) { imageQueue.push({ id, imageSrc }); if (!isProcessing) { processQueue(); } } function processQueue() { if (imageQueue.length === 0) { isProcessing = false; return; } isProcessing = true; if (!finishedInitialLoading) { setTimeout(processQueue, 100); return; } const { id, imageSrc } = imageQueue.shift(); const hwtype = $('#tag' + id).dataset.hwtype; if (tagTypes[hwtype]?.busy) { imageQueue.push({ id, imageSrc }); setTimeout(processQueue, 100); return; }; const canvas = $('#tag' + id + ' .tagimg'); canvas.style.display = 'block'; fetch(imageSrc, { cache: "force-cache" }) .then(response => response.arrayBuffer()) .then(buffer => { [canvas.width, canvas.height] = [tagTypes[hwtype].width, tagTypes[hwtype].height] || [0, 0]; if (tagTypes[hwtype].rotatebuffer) [canvas.width, canvas.height] = [canvas.height, canvas.width]; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(canvas.width, canvas.height); const data = new Uint8ClampedArray(buffer); if (data.length == 0) canvas.style.display = 'none'; if (tagTypes[hwtype].bpp == 16) { const is16Bit = data.length == tagTypes[hwtype].width * tagTypes[hwtype].height * 2; for (let i = 0; i < min(tagTypes[hwtype].width * tagTypes[hwtype].height, data.length); i++) { const dataIndex = is16Bit ? i * 2 : i; const rgb = is16Bit ? (data[dataIndex] << 8) | data[dataIndex + 1] : data[dataIndex]; imageData.data[i * 4] = is16Bit ? ((rgb >> 11) & 0x1F) << 3 : (((rgb >> 5) & 0x07) << 5) * 1.13; imageData.data[i * 4 + 1] = is16Bit ? ((rgb >> 5) & 0x3F) << 2 : (((rgb >> 2) & 0x07) << 5) * 1.13; imageData.data[i * 4 + 2] = is16Bit ? (rgb & 0x1F) << 3 : ((rgb & 0x03) << 6) * 1.3; imageData.data[i * 4 + 3] = 255; } } else { const offsetRed = (data.length >= (canvas.width * canvas.height / 8) * 2) ? canvas.width * canvas.height / 8 : 0; let pixelValue = 0; const colorTable = tagTypes[hwtype].colortable; for (let i = 0; i < data.length; i++) { for (let j = 0; j < 8; j++) { const pixelIndex = i * 8 + j; if (offsetRed) { pixelValue = ((data[i] & (1 << (7 - j))) ? 1 : 0) | (((data[i + offsetRed] & (1 << (7 - j))) ? 1 : 0) << 1); } else { pixelValue = ((data[i] & (1 << (7 - j))) ? 1 : 0); } imageData.data[pixelIndex * 4] = colorTable[pixelValue][0]; imageData.data[pixelIndex * 4 + 1] = colorTable[pixelValue][1]; imageData.data[pixelIndex * 4 + 2] = colorTable[pixelValue][2]; imageData.data[pixelIndex * 4 + 3] = 255; } } } ctx.putImageData(imageData, 0, 0); processQueue(); }) .catch(error => { processQueue(); }); } function displayTime(seconds) { let hours = Math.floor(Math.abs(seconds) / 3600); let minutes = Math.floor((Math.abs(seconds) % 3600) / 60); let remainingSeconds = Math.abs(seconds) % 60; return (seconds < 0 ? '-' : '') + (hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}` : `${minutes}`) + `:${String(remainingSeconds).padStart(2, '0')}`; } $("#filterOptions").addEventListener("click", function (event) { if (event.target.tagName === "INPUT") { GroupSortFilter(); } }); function GroupSortFilter() { const sortableGrid = $('#taglist'); const gridItems = Array.from(sortableGrid.querySelectorAll('.tagcard:not(#tagtemplate)')); let grouping = document.querySelector('input[name="group"]:checked')?.value; let sorting = document.querySelector('input[name="sort"]:checked')?.value ?? 'alias'; gridItems.sort((a, b) => { let itemA = String(sorting).startsWith('data-') ? a.dataset[sorting.slice(5)] : a.querySelector('.' + sorting).textContent; let itemB = String(sorting).startsWith('data-') ? b.dataset[sorting.slice(5)] : b.querySelector('.' + sorting).textContent; if (sorting == 'data-lastseen') [itemA, itemB] = [itemB, itemA]; if (grouping) { let groupA = String(grouping).startsWith('data-') ? a.dataset[grouping.slice(5)] : a.querySelector('.' + grouping).textContent; let groupB = String(grouping).startsWith('data-') ? b.dataset[grouping.slice(5)] : b.querySelector('.' + grouping).textContent; if (groupA !== groupB) { return groupA.localeCompare(groupB); } else { return itemA.localeCompare(itemB); } } else { return itemA.localeCompare(itemB); } }); let currentGroup = null; let order = 1; let headItems = Array.from($('#taglist').getElementsByClassName('taggroup')); headItems.forEach(item => { item.dataset.clean = 1; }) gridItems.forEach(item => { if (grouping) { const group = String(grouping).startsWith('data-') ? item.dataset[grouping.slice(5)] || '' : item.querySelector('.' + grouping).textContent || ''; if (group !== currentGroup && group != '') { let header = document.getElementById('header' + group); if (!header) { header = document.createElement('div'); switch (grouping) { case 'model': header.textContent = 'Tag model: ' + group; break; case 'contentmode': header.textContent = 'Content: ' + group; break; case 'data-channel': header.textContent = 'Channel: ' + group; break; } header.classList.add('taggroup'); header.id = 'header' + group; sortableGrid.appendChild(header); } header.style.order = order++; header.dataset.clean = 0; currentGroup = group; } } let show = true; if ($('input[name="filter"][value="remote"]').checked && item.dataset.isexternal == "false") show = false; if ($('input[name="filter"][value="local"]').checked && item.dataset.isexternal == "true") show = false; if ($('input[name="filter"][value="inactive"]').checked && item.querySelector('.warningicon').style.display != 'inline-block') show = false; if ($('input[name="filter"][value="pending"]').checked && !item.classList.contains("tagpending")) show = false; if (!show) item.style.display = 'none'; else item.style.display = 'block'; item.style.order = order++; }); headItems = Array.from($('#taglist').getElementsByClassName('taggroup')); headItems.forEach(item => { if (item.dataset.clean == 1) item.parentNode.removeChild(item); }) } $('#toggleFilters').addEventListener('click', () => { event.preventDefault(); const filterOptions = $('#filterOptions'); filterOptions.classList.toggle('active'); if (filterOptions.classList.contains('active')) { filterOptions.style.maxHeight = filterOptions.scrollHeight + 20 + 'px'; } else { filterOptions.style.maxHeight = 0; } }); async function getTagtype(hwtype) { if (tagTypes[hwtype]) { return tagTypes[hwtype]; } if (getTagtypeBusy) { await new Promise(resolve => { const checkBusy = setInterval(() => { if (!getTagtypeBusy) { clearInterval(checkBusy); resolve(); } }, 50); }); } if (tagTypes[hwtype]?.busy) { await new Promise(resolve => { const checkBusy = setInterval(() => { if (!tagTypes[hwtype].busy) { clearInterval(checkBusy); resolve(); } }, 10); }); } if (tagTypes[hwtype]) { return tagTypes[hwtype]; } try { tagTypes[hwtype] = { busy: true }; getTagtypeBusy = true; const response = await fetch('/tagtypes/' + hwtype.toString(16).padStart(2, '0').toUpperCase() + '.json'); if (!response.ok) { let data = { name: 'unknown id ' + hwtype, width: 0, height: 0, bpp: 0, rotatebuffer: 0, colortable: [], busy: false }; tagTypes[hwtype] = data; getTagtypeBusy = false; return data; } const jsonData = await response.json(); let data = { name: jsonData.name, width: parseInt(jsonData.width), height: parseInt(jsonData.height), bpp: parseInt(jsonData.bpp), rotatebuffer: jsonData.rotatebuffer, colortable: Object.values(jsonData.colortable), contentids: Object.values(jsonData.contentids ?? []), busy: false }; tagTypes[hwtype] = data; getTagtypeBusy = false; return data; } catch (error) { console.error('Error fetching data:', error); getTagtypeBusy = false; return null; } } function dropUpload() { const dropZone = $('#taglist'); let timeoutId; dropZone.addEventListener('dragenter', (event) => { const tagCard = event.target.closest('.tagcard'); tagCard?.classList.add('drophighlight'); }); dropZone.addEventListener('dragover', (event) => { event.preventDefault(); const tagCard = event.target.closest('.tagcard'); tagCard?.classList.add('drophighlight'); }); dropZone.addEventListener('dragleave', (event) => { const tagCard = event.target.closest('.tagcard'); tagCard?.classList.remove('drophighlight'); }); dropZone.addEventListener('drop', (event) => { event.preventDefault(); const file = event.dataTransfer.files[0]; const tagCard = event.target.closest('.tagcard'); const mac = tagCard.dataset.mac; if (tagCard && file && file.type.startsWith('image/')) { const itemId = tagCard.id; const reader = new FileReader(); reader.onload = function (e) { const image = new Image(); image.src = e.target.result; image.onload = function () { const hwtype = tagCard.dataset.hwtype; const [width, height] = [tagTypes[hwtype].width, tagTypes[hwtype].height] || [0, 0]; const canvas = createCanvas(width, height); const ctx = canvas.getContext('2d'); const scaleFactor = Math.max( canvas.width / image.width, canvas.height / image.height ); const newWidth = image.width * scaleFactor; const newHeight = image.height * scaleFactor; const x = (canvas.width - newWidth) / 2; const y = (canvas.height - newHeight) / 2; ctx.drawImage(image, x, y, newWidth, newHeight); canvas.toBlob(async (blob) => { const formData = new FormData(); formData.append('mac', mac); formData.append('file', blob, 'image.jpg'); try { const response = await fetch('/imgupload', { method: 'POST', body: formData, }); if (response.ok) { console.log('Resized image uploaded successfully'); } else { console.error('Image upload failed'); } } catch (error) { console.error('Image upload failed', error); } }, 'image/jpeg'); }; image.onerror = function () { console.error('Failed to load image.'); }; }; reader.readAsDataURL(file); } else if (file.type === 'application/json') { const reader = new FileReader(); reader.onload = function (event) { const jsonContent = event.target.result; const formData = new FormData(); formData.append('mac', mac); formData.append('json', jsonContent); fetch('/jsonupload', { method: 'POST', body: formData, }) .then(response => { if (response.ok) { console.log('JSON uploaded successfully'); } else { console.error('JSON upload failed'); } }) .catch(error => { console.error('JSON upload failed', error); }); }; reader.readAsText(file); } tagCard.classList.remove('drophighlight'); }); function createCanvas(width, height) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } } const contextMenu = $('#context-menu'); $('#taglist').addEventListener('contextmenu', (e) => { console.log("contextmenu"); e.preventDefault(); const clickedGridItem = e.target.closest('.tagcard'); if (clickedGridItem) { let mac = clickedGridItem.dataset.mac; console.log("tagcard"); let contextMenuOptions = [ { id: 'refresh', label: 'Force refresh' }, { id: 'clear', label: 'Clear pending status' } ]; if (clickedGridItem.dataset.isexternal == "false") { contextMenuOptions.push( { id: 'scan', label: 'Scan channels' }, { id: 'reboot', label: 'Reboot tag' }, ); }; contextMenuOptions.push( { id: 'del', label: 'Delete tag from list' } ); contextMenu.innerHTML = ''; contextMenuOptions.forEach(option => { const li = document.createElement('li'); li.textContent = option.label; li.addEventListener('click', (e) => { e.preventDefault(); console.log(`${option.id} clicked for ${mac}`); sendCmd(mac, option.id); contextMenu.style.display = 'none'; }); contextMenu.appendChild(li); }); const contextMenuPosition = { left: e.clientX, top: e.clientY }; contextMenu.style.left = `${contextMenuPosition.left}px`; contextMenu.style.top = `${contextMenuPosition.top}px`; contextMenu.style.display = 'block'; } }); document.addEventListener('click', () => { contextMenu.style.display = 'none'; }); function populateTimes(element) { for (let i = 0; i < 24; i++) { const option = document.createElement("option"); option.value = i; option.text = i.toString().padStart(2, "0") + ":00"; element.appendChild(option); } }