Files
OpenEPaperLink/ESP32_AP-Flasher/wwwroot/main.js
Nic Limper d2135a57bd bugfix
2024-01-09 21:35:42 +01:00

1568 lines
50 KiB
JavaScript

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_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 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;
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;
}
});
});
window.addEventListener("load", function () {
initTabs();
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?");
});
window.dispatchEvent(loadConfig);
dropUpload();
populateTimes($('#apcnight1'));
populateTimes($('#apcnight2'));
});
/* tabs */
let activeTab = '';
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");
});
document.getElementById(targetId).style.display = "block";
this.classList.add("active");
});
});
tabLinks[0].click();
};
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() {
protocol = location.protocol == "https:" ? "wss://" : "ws://";
socket = new WebSocket(protocol + location.host + "/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 <span class="blink-red" title="Generating content is paused">FULL! ${convertSize(
msg.sys.littlefsfree
)} </span>`;
} else {
str += `filesystem free: ${convertSize(msg.sys.littlefsfree)}`;
}
$("#sysinfo").innerHTML = str;
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;
$('#dashboardStatus').innerHTML = apstate[msg.sys.apstate].state;
}
servertimediff = (Date.now() / 1000) - msg.sys.currtime;
}
if (msg.apitem) {
populateAPCard(msg.apitem);
}
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;
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 ($('#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);
$('#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)}`;
}
})();
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 ? "&#x2265;" : "") + (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 (!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') {
//cachetag = Math.random();
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;
}
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 = "<span>next update</span>" + 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 || 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 = "<font color=yellow>First boot</font>"
$('#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_NFC:
$('#tag' + tagmac + ' .nextcheckin').innerHTML = "NFC wakeup"
$('#tag' + tagmac).style.background = "#c8f1bb";
break;
case WAKEUP_REASON_NETWORK_SCAN:
$('#tag' + tagmac + ' .nextcheckin').innerHTML = "<font color=yellow>Network scan</font>"
$('#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');
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;
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 = "<span>last seen</span>" + 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';
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 = "<span>expected checkin</span>" + displayTime(Math.floor(nextcheckin));
} else {
$('#tag' + tagmac + ' .nextcheckin').innerHTML = "";
}
})
$('#dashboardTagCount').innerHTML = tagcount;
$('#dashboardPending').innerHTML = pendingcount;
$('#dashboardLowBatt').innerHTML = lowbattcount;
$('#dashboardTimeout').innerHTML = timeoutcount;
}
$('#clearlog').onclick = function () {
$('#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 = '&#x25BC;';
$('#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' ? '&#x25B2;' : '&#x25BC;';
$('#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();
}
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 || 'jjwbruijn/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 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 md5 = info[0][tagtype]["md5"];
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 => {
apConfig = data;
$('#apcfgalias').value = data.alias;
$('#apcfgchid').value = data.channel;
$("#apcfgledbrightness").value = data.led;
$("#apcfgtftbrightness").value = data.tft;
$("#apcfglanguage").value = data.language;
$("#apclatency").value = data.maxsleep;
$("#apcpreventsleep").value = data.stopsleep;
$("#apcpreview").value = data.preview;
$("#apclock").value = data.lock;
$("#apcwifipower").value = data.wifipower;
$("#apctimezone").value = data.timezone;
$("#apcnight1").value = data.sleeptime1;
$("#apcnight2").value = data.sleeptime2;
})
$('#apcfgmsg').innerHTML = '';
break;
case 'updatetab':
$('#updateconsole').innerHTML = '';
loadOTA();
break;
}
});
$('#apcfgsave').onclick = function () {
let formData = new FormData();
formData.append("alias", $('#apcfgalias').value);
formData.append("channel", $('#apcfgchid').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('lock', $('#apclock').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);
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 => {
console.log('File uploaded successfully: ', data);
alert('TagDB restored. Webpage will reload.');
location.reload();
})
.catch(error => {
console.error('Error uploading file:', error);
alert('Error uploading file: ' + error);
});
} else {
console.error('No file selected.');
alert('No file selected');
}
}
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 = "<div id=\"buttonbar\"></div><div id=\"canvasdiv\"></div><div id=\"layersdiv\"></div><p id=\"savebar\"></p>";
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 = "<p>" + contentDef?.desc + "</p>"
}
$('#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':
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;
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)) {
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);
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", '<li class="new">' + htmlEncode(time) + ' <span class="mono">' + message.replace(/"([^"]+)"/g, '"<span class="quote">$1</span>"') + '</span></li>');
} else if (iserr) {
messages.insertAdjacentHTML("afterbegin", '<li class="new error">' + htmlEncode(time + ' ' + message) + '</li>');
} else {
messages.insertAdjacentHTML("afterbegin", '<li class="new">' + htmlEncode(time + ' ' + message) + '</li>');
const hexRegex = /^[0-9A-Fa-f]{16}.*/;
if (hexRegex.test(message.substring(0, 16))) {
let div = $('#tag' + message.substring(0, 16));
if (div) {
div.classList.add("tagxfer");
(function (tagmac) {
setTimeout(function () { $('#tag' + tagmac).classList.remove("tagxfer"); }, 200);
})(message.substring(0, 16));
}
}
}
}
function htmlEncode(input) {
const textArea = document.createElement("textarea");
textArea.innerText = input;
return textArea.innerHTML.split("<br>").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;
if (grouping == undefined) {
grouping = localStorage.getItem("grouping");
if (grouping) document.querySelector('input[name="group"][value="' + grouping + '"]').checked = true
} else {
localStorage.setItem("grouping", grouping);
}
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;
let batteryMv = tagDB[item.dataset.mac].batteryMv;
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 ($('input[name="filter"][value="lowbatt"]').checked && (batteryMv >= 2400 || batteryMv == 0 || batteryMv == 1337)) show = false;
if (!show) item.style.display = 'none'; else item.style.display = 'block';
item.style.order = order++;
const checkedValues = Array.from(document.querySelectorAll('input[name="filter"]:checked'))
.map(checkbox => checkbox.value)
.join(', ');
$('#activefilter').innerHTML = (checkedValues ? 'filtered by ' + checkedValues : '');
});
headItems = Array.from($('#taglist').getElementsByClassName('taggroup'));
headItems.forEach(item => {
if (item.dataset.clean == 1) item.parentNode.removeChild(item);
})
}
$('#toggleFilters').addEventListener('click', (event) => {
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;
}
});
$('#activefilter').addEventListener('click', (event) => {
event.preventDefault();
const filterOptions = $('#filterOptions');
filterOptions.classList.add('active');
filterOptions.style.maxHeight = filterOptions.scrollHeight + 20 + 'px';
});
async function getTagtype(hwtype) {
if (tagTypes[hwtype] && tagTypes[hwtype].busy == false) {
return tagTypes[hwtype];
}
// nice, but no possibility to invalidate this cache yet.
/*
const storedData = JSON.parse(localStorage.getItem("tagTypes"));
if (storedData && storedData[hwtype]) {
return storedData[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();
}
}, 50);
});
}
if (tagTypes[hwtype]) {
return tagTypes[hwtype];
}
try {
getTagtypeBusy = true;
tagTypes[hwtype] = { busy: true };
const response = await fetch('/tagtypes/' + hwtype.toString(16).padStart(2, '0').toUpperCase() + '.json');
if (!response.ok) {
let data = { name: 'unknown id ' + hwtype.toString(16), 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 ?? []),
options: Object.values(jsonData.options ?? []),
busy: false
};
tagTypes[hwtype] = data;
localStorage.setItem("tagTypes", JSON.stringify(tagTypes));
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) => {
e.preventDefault();
const clickedGridItem = e.target.closest('.tagcard');
if (clickedGridItem) {
let mac = clickedGridItem.dataset.mac;
const hwtype = clickedGridItem.dataset.hwtype;
let contextMenuOptions = [];
if (tagTypes[hwtype]?.width > 0) {
contextMenuOptions.push(
{ 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' },
);
};
if (tagTypes[hwtype]?.options?.includes("led")) {
contextMenuOptions.push(
{ id: 'ledflash', label: 'Flash the LED' },
{ id: 'ledflash_long', label: 'Flash the LED (long)' },
{ id: 'ledflash_stop', label: 'Stop flashing' }
);
}
}
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 + window.scrollX,
top: e.clientY + window.scrollY
};
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);
}
}
function populateAPCard(msg) {
let apip = msg.ip;
let apid = apip.replace(/\./g, "-");
if (!$('#ap' + apid)) {
div = $('#apcard').cloneNode(true);
div.setAttribute('id', 'ap' + apid);
$('#aplist').appendChild(div);
}
let alias = msg.alias;
if (!alias) alias = apip;
$('#ap' + apid + ' .apip').innerHTML = "<a href=\"http://" + apip + "\" target=\"_new\">" + apip + "</a>";
$('#ap' + apid + ' .apalias').innerHTML = alias;
$('#ap' + apid + ' .aptagcount').innerHTML = msg.count;
$('#ap' + apid + ' .apchannel').innerHTML = msg.channel;
const elements = document.querySelectorAll('.apchannel');
Array.from(elements).forEach(element => {
if (element.textContent === msg.channel && element.id !== 'ap' + apid) {
$('#ap' + apid + ' .apchannel').style.color = 'red';
$('#ap' + apid + ' .apchannel').innerHTML += ' conflict';
}
});
// $('#ap' + apid + ' .apversion').innerHTML = msg.version;
if (activeTab == 'aptab') {
populateAPInfo(apip);
}
}
function populateAPInfo(apip) {
let apid = apip.replace(/\./g, "-");
fetch('http://' + apip + '/sysinfo')
.then(response => {
if (response.status != 200) {
$('#ap' + apid + ' .apswversion').innerHTML = "Error fetching sysinfo: " + response.status;
return {};
} else {
return response.json();
}
})
.then(data => {
if (data.env) {
let version = '';
version += `env: ${data.env}<br>`;
version += `build date: ${formatEpoch(data.buildtime)}<br>`;
version += `esp32 version: ${data.buildversion}<br>`;
version += `psram size: ${data.psramsize}<br>`;
version += `flash size: ${data.flashsize}<br>`;
$('#ap' + apid + ' .apswversion').innerHTML = version;
// if (data.env == 'ESP32_S3_16_8_YELLOW_AP') $("#c6Option").style.display = 'block';
}
})
.catch(error => {
$('#ap' + apid + ' .apswversion').innerHTML = "Error fetching sysinfo: " + error;
});
}
function formatEpoch(epochTime) {
const date = new Date(epochTime * 1000); // Convert seconds to milliseconds
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
function setFilterAndShow(filter) {
$('input[name="filter"][value="remote"]').checked = false;
$('input[name="filter"][value="local"]').checked = false;
$('input[name="filter"][value="inactive"]').checked = (filter == 'inactive');
$('input[name="filter"][value="pending"]').checked = (filter == 'pending');
$('input[name="filter"][value="lowbatt"]').checked = (filter == 'lowbatt');
GroupSortFilter();
$(`[data-target='tagtab']`).click();
}
// geocoding typeahead
async function searchLocations() {
const query = $(".geoselect").value.trim();
document.getElementById('opt#lat').value = '';
document.getElementById('opt#lon').value = '';
if (document.getElementById('opt#tz')) document.getElementById('opt#tz').value = '';
if (query.length === 0) {
$('#georesults').innerHTML = '';
return;
}
try {
const response = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${query}&count=10&language=en&format=json`);
const data = await response.json();
displayResults(data.results);
} catch (error) {
console.error('Error fetching data:', error);
}
}
function displayResults(results) {
$('#georesults').innerHTML = '';
$('#georesults').style.top = $(".geoselect").offsetTop + $(".geoselect").offsetHeight + "px";
$('#georesults').style.left = $(".geoselect").offsetLeft + 'px';
// $('#georesults').style.width = $(".geoselect").offsetWidth + 'px';
if (results) {
results.forEach(result => {
const option = document.createElement('div');
option.textContent = result.name + ', ' + result.admin1 + ', ' + result.country;
option.addEventListener('click', () => selectLocation(result));
$('#georesults').appendChild(option);
});
}
}
function selectLocation(location) {
$(".geoselect").value = location.name;
document.getElementById('opt#lat').value = location.latitude;
document.getElementById('opt#lon').value = location.longitude;
if (document.getElementById('opt#tz')) document.getElementById('opt#tz').value = location.timezone;
$('#georesults').innerHTML = '';
console.log('Selected location:', location);
}
function debounce(func, delay) {
let timeoutId;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}