From a0f7c84d30c684db6d9decc2ae990162eff76df5 Mon Sep 17 00:00:00 2001 From: Nic Limper Date: Wed, 1 Feb 2023 19:33:52 +0100 Subject: [PATCH] big webpage overhaul (not production ready yet, but feel free to try!) --- esp32_fw/data/index.html | 61 +++----- esp32_fw/data/kat-bw29.jpg | Bin 0 -> 26890 bytes esp32_fw/data/main.css | 15 +- esp32_fw/data/main.js | 170 +++++++++++++++------- esp32_fw/data/numbers1-1.vlw | Bin 0 -> 55066 bytes esp32_fw/data/numbers1-2.vlw | Bin 0 -> 90373 bytes esp32_fw/data/numbers2-1.vlw | Bin 0 -> 22861 bytes esp32_fw/data/numbers2-2.vlw | Bin 0 -> 47218 bytes esp32_fw/data/numbers3-1.vlw | Bin 0 -> 14076 bytes esp32_fw/data/numbers3-2.vlw | Bin 0 -> 22861 bytes esp32_fw/include/commstructs.h | 4 +- esp32_fw/include/contentmanager.h | 15 ++ esp32_fw/include/makeimage.h | 3 +- esp32_fw/include/pendingdata.h | 2 + esp32_fw/include/tag_db.h | 41 ++++-- esp32_fw/include/web.h | 5 +- esp32_fw/src/contentmanager.cpp | 228 ++++++++++++++++++++++++++++++ esp32_fw/src/main.cpp | 21 +-- esp32_fw/src/makeimage.cpp | 23 +-- esp32_fw/src/newproto.cpp | 77 ++++++---- esp32_fw/src/serial.cpp | 1 - esp32_fw/src/tag_db.cpp | 161 ++++++++++++++++++--- esp32_fw/src/web.cpp | 194 ++----------------------- 23 files changed, 650 insertions(+), 371 deletions(-) create mode 100644 esp32_fw/data/kat-bw29.jpg create mode 100644 esp32_fw/data/numbers1-1.vlw create mode 100644 esp32_fw/data/numbers1-2.vlw create mode 100644 esp32_fw/data/numbers2-1.vlw create mode 100644 esp32_fw/data/numbers2-2.vlw create mode 100644 esp32_fw/data/numbers3-1.vlw create mode 100644 esp32_fw/data/numbers3-2.vlw create mode 100644 esp32_fw/include/contentmanager.h create mode 100644 esp32_fw/src/contentmanager.cpp diff --git a/esp32_fw/data/index.html b/esp32_fw/data/index.html index ea1ffddd..9cd7fc7a 100644 --- a/esp32_fw/data/index.html +++ b/esp32_fw/data/index.html @@ -18,12 +18,12 @@

00000000

- - + +

- - @@ -31,17 +31,19 @@

- - - - - + + + +

+

-
- - -
-
-
-

- - -

-

- - min -

-

- -

-
-
-

- - -

-

- -

-
-
-

- -

-
-
+
+ Currently active tags:
+
@@ -96,9 +68,10 @@
-
- last: +
+
+
diff --git a/esp32_fw/data/kat-bw29.jpg b/esp32_fw/data/kat-bw29.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae6c765e530ed3906137af923f2709b704822c38 GIT binary patch literal 26890 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 HKTrP`XsZFp literal 0 HcmV?d00001 diff --git a/esp32_fw/data/main.css b/esp32_fw/data/main.css index f7ae66c3..183074a4 100644 --- a/esp32_fw/data/main.css +++ b/esp32_fw/data/main.css @@ -82,7 +82,7 @@ input { input[type=button] { border: 0px; - padding: 4px; + padding: 4px 10px; cursor:pointer; } input[type=button]:hover { @@ -113,6 +113,15 @@ select { font-weight: bold; } +#configbox input { + border: solid 1px #666666; + padding: 4px; +} + +#configbox label { + text-transform: capitalize; +} + #cfgdelete { position: absolute; bottom: 15px; @@ -167,6 +176,10 @@ select { background-color: #dddddd; } +.tagcard .pending { + padding-bottom:15px; +} + .currimg { float: right; } diff --git a/esp32_fw/data/main.js b/esp32_fw/data/main.js index 64410a45..bf4c1890 100644 --- a/esp32_fw/data/main.js +++ b/esp32_fw/data/main.js @@ -1,21 +1,35 @@ const $ = document.querySelector.bind(document); -const contentModes = ["static image", "current date", "count days", "count hours","current weather","public transport","memo text"]; -const models = ["unknown", "1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"]; +const contentModes = ["static image", "current date", "counting days", "counting hours", "current weather", "firmware update", "memo text", "image url"]; +const models = ["unknown type", "1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"]; +const contentModeOptions = []; +contentModeOptions[0] = ["filename","timetolive"]; +contentModeOptions[1] = []; +contentModeOptions[2] = ["counter", "thresholdred"]; +contentModeOptions[3] = ["counter", "thresholdred"]; +contentModeOptions[4] = ["location"]; +contentModeOptions[5] = ["filename"]; +contentModeOptions[6] = ["text"]; +contentModeOptions[7] = ["url","interval"]; +const imageQueue = []; +let isProcessing = false; +let servertimediff = 0; let socket; connect(); setInterval(updatecards, 1000); -window.addEventListener("load", function () { - fetch("/get_db") +window.addEventListener("load", function () { loadTags(0) }); + +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); }) - .catch(error => showMessage('Error: ' + error)); -}); - + //.catch(error => showMessage('loadTags error: ' + error)); +} function connect() { socket = new WebSocket("ws://" + location.host + "/ws"); @@ -35,6 +49,7 @@ function connect() { } if (msg.sys) { $('#sysinfo').innerHTML = 'free heap: ' + msg.sys.heap + ' bytes ┇ db size: ' + msg.sys.dbsize + ' bytes ┇ db record count: ' + msg.sys.recordcount + ' ┇ littlefs free: ' + msg.sys.littlefsfree + ' bytes'; + servertimediff = (Date.now() / 1000) - msg.sys.currtime; } }); @@ -69,52 +84,51 @@ function processTags(tagArray) { if (!alias) alias = tagmac; $('#tag' + tagmac + ' .alias').innerHTML = alias; - var img = $('#tag' + tagmac + ' .tagimg'); - img.style.display = 'block'; - img.src = '/edit?edit=current/' + tagmac + '.bmp&' + (new Date()).getTime(); + if (div.dataset.hash != element.hash) loadImage(tagmac, '/current/' + tagmac + '.bmp?' + (new Date()).getTime()); $('#tag' + tagmac + ' .contentmode').innerHTML = contentModes[element.contentmode]; $('#tag' + tagmac + ' .model').innerHTML = models[element.model]; - var date = new Date(element.nextupdate * 1000); - var options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; - $('#tag' + tagmac + ' .nextupdate').innerHTML = date.toLocaleString('nl-NL', options).replace(',', ''); + if (element.nextupdate > 1672531200 && element.nextupdate!=3216153600) { + var date = new Date(element.nextupdate * 1000); + var 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 = ""; + } - date = new Date(element.lastseen * 1000); - var options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false}; - $('#tag' + tagmac + ' .lastseen').innerHTML = date.toLocaleString('nl-NL', options).replace(',', ''); + if (element.nextcheckin > 1672531200) { + div.dataset.nextcheckin = element.nextcheckin; + } else { + div.dataset.nextcheckin = element.lastseen + 1800; + } div.dataset.lastseen = element.lastseen; - div.dataset.lastseenlocaltime = Date.now(); + div.dataset.hash = element.hash; $('#tag' + tagmac + ' .warningicon').style.display = 'none'; - if (element.pending) $('#tag' + tagmac + ' .pending').innerHTML = "pending..."; else $('#tag' + tagmac + ' .pending').innerHTML = ""; + if (element.pending) $('#tag' + tagmac + ' .pending').innerHTML = "pending update..."; else $('#tag' + tagmac + ' .pending').innerHTML = ""; } } function updatecards() { document.querySelectorAll('[data-mac]').forEach(item => { - var tagmac = item.dataset.mac; - var idletime = Date.now() - item.dataset.lastseenlocaltime; - $('#tag' + tagmac + ' .idletime').innerHTML = int(idletime); - if (idletime > 300000) $('#tag' + tagmac + ' .warningicon').style.display='inline-block'; - if (idletime > 1800000) $('#tag' + tagmac).style.display = 'none'; - }) -} + let tagmac = item.dataset.mac; -$('#send_image').onclick = function() { - let formData = new FormData(); - formData.append("dst", $("#dstmac").value); - formData.append("filename", $("#imgfile").value); - formData.append("ttl", $("#ttl").value); - fetch("/send_image", { - method: "POST", - body: formData + if (item.dataset.lastseen && item.dataset.lastseen > 1672531200) { + 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 > item.dataset.nextcheckin) $('#tag' + tagmac + ' .warningicon').style.display='inline-block'; + } else { + $('#tag' + tagmac + ' .lastseen').innerHTML = "" + } + + if (item.dataset.nextcheckin > 1672531200) { + let nextcheckin = item.dataset.nextcheckin - ((Date.now() / 1000) + servertimediff); + $('#tag' + tagmac + ' .nextcheckin').innerHTML = "expecting next checkin: " + displayTime(Math.floor(nextcheckin)); + } }) - .then(response => response.text()) - .then(data => showMessage(data)) - .catch(error => showMessage('Error: ' + error)); } $('#send_fw').onclick = function () { @@ -130,18 +144,6 @@ $('#send_fw').onclick = function () { .catch(error => showMessage('Error: ' + error)); } -$('#req_checkin').onclick = function () { - let formData = new FormData(); - formData.append("dst", $("#dstmac").value); - fetch("/req_checkin", { - method: "POST", - body: formData - }) - .then(response => response.text()) - .then(data => showMessage(data)) - .catch(error => showMessage('Error: ' + error)); -} - $('#clearlog').onclick = function () { $('#messages').innerHTML=''; } @@ -176,6 +178,8 @@ $('#taglist').addEventListener("click", (event) => { $('#cfgalias').value = tagdata.alias; $('#cfgcontent').value = tagdata.contentmode; $('#cfgmodel').value = tagdata.model; + $('#cfgcontent').dataset.json = tagdata.modecfgjson; + contentselected(); $('#configbox').style.display = 'block'; }) .catch(error => showMessage('Error: ' + error)); @@ -183,11 +187,20 @@ $('#taglist').addEventListener("click", (event) => { }) $('#cfgsave').onclick = function () { + + let contentmode = $('#cfgcontent').value; + let extraoptions = contentModeOptions[contentmode]; + let obj={}; + extraoptions.forEach(element => { + obj[element] = $('#opt' + element).value; + }); + let formData = new FormData(); formData.append("mac", $('#cfgmac').dataset.mac); formData.append("alias", $('#cfgalias').value); - formData.append("contentmode", $('#cfgcontent').value); + formData.append("contentmode", contentmode); formData.append("model", $('#cfgmodel').value); + formData.append("modecfgjson", JSON.stringify(obj)); fetch("/save_cfg", { method: "POST", body: formData @@ -202,6 +215,30 @@ $('#cfgdelete').onclick = function () { let mac = $('#cfgmac').dataset.mac; } +function contentselected() { + let contentmode=$('#cfgcontent').value; + let extraoptions = contentModeOptions[contentmode]; + $('#customoptions').innerHTML=""; + var obj = {}; + if ($('#cfgcontent').dataset.json && ($('#cfgcontent').dataset.json!="null")) { + obj = JSON.parse($('#cfgcontent').dataset.json); + } + console.log(obj); + extraoptions.forEach(element => { + var label = document.createElement("label"); + label.innerHTML = element; + label.setAttribute("for", 'opt' + element); + var input = document.createElement("input"); + input.type = "text"; + input.id = 'opt' + element; + if (obj[element]) input.value = obj[element]; + var p = document.createElement("p"); + p.appendChild(label); + p.appendChild(input); + $('#customoptions').appendChild(p); + }); +} + function showMessage(message) { const messages = $('#messages'); var date = new Date(), @@ -213,4 +250,37 @@ function htmlEncode(input) { const textArea = document.createElement("textarea"); textArea.innerText = input; return textArea.innerHTML.split("
").join("\n"); -} \ No newline at end of file +} + +function loadImage(id, imageSrc) { + imageQueue.push({ id, imageSrc }); + if (!isProcessing) { + processQueue(); + } +} + +function processQueue() { + if (imageQueue.length === 0) { + isProcessing = false; + return; + } + isProcessing = true; + const { id, imageSrc } = imageQueue.shift(); + const image = $('#tag' + id + ' .tagimg'); + image.onload = function () { + image.style.display = 'block'; + processQueue(); + } + image.onerror = function () { + image.style.display = 'none'; + processQueue(); + }; + image.src = imageSrc; +} + +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')}`; +} diff --git a/esp32_fw/data/numbers1-1.vlw b/esp32_fw/data/numbers1-1.vlw new file mode 100644 index 0000000000000000000000000000000000000000..e5e2e87a3caeb5b1a3e8bee6b26907f4982ebe11 GIT binary patch literal 55066 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 literal 0 HcmV?d00001 diff --git a/esp32_fw/data/numbers1-2.vlw b/esp32_fw/data/numbers1-2.vlw new file mode 100644 index 0000000000000000000000000000000000000000..c6f615be6ce4a5dd9b4faf6c7a4734b2c2f1d133 GIT binary patch literal 90373 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 T7eD*SU;g{g|7~`^_vro~&SaT2 literal 0 HcmV?d00001 diff --git a/esp32_fw/data/numbers2-1.vlw b/esp32_fw/data/numbers2-1.vlw new file mode 100644 index 0000000000000000000000000000000000000000..1dbfa21b37c389e0e6156dd673a762805ba42e0f GIT binary patch literal 22861 zcmeI0S&L*v5QPgB1ogq6Ac!j{?km3Nzwk}*Wne(?|3e$OM8-L>+{)IP?waW;xN{>; ze0ef5?yZ_(PN&mbr_u0$>k?V80ev<2Tx!UJTxqg)EeYsk# z#c#^hK3~bz*bn8>b#F-AK3~h#JhR$2qBoyUh7#kUDtSHH9lI7q~8Ba7_%wvt<;@ku1I_?4>c{aV`LSYNMV@KSWD*~2=zH}X+ zI}0~?(Bcim;oP-1gg;!ij48>i#L*4x6X2m(c9HS$^u48R_A-30rF&@Dy^&aH$MpF+ zx-a;A3&d|jG(g43L?vk+h&Ano)g;>mmE#FTL*tG~(@YSMER897nHF;Z%Z1|L!A zS#W0L-bv*V$Pp&&olytuVz8^CfwPcQEAt)!k7NY7qyV`T1iIW0p_9XjmWyaBmLNfz zbcl>K2Z)*$$O(3>5Vfd#0itU6hQN4n7!U&ij|G78WkBZUg43}0HW!-(MlW_Q=$^*( zgOL)iqImw)_=xCx&xMu6DXo$Q5$TGS`@LYAyto4fuw!bvoq^`27oz%}XT2OXPaT#u zeTr)^OEFq0U{o?_ZploVnO0SrJtIIPO5qu#>B; zlpftuT|iKH?*`0Fd^Jq^+^;{qfWiU zsQL}^!yI|~rw%Rr-Hr4OvNxLk3dA8v{37OLp5YN;iDGPv3NBfk(>Ya&Ri91D%Pt?3 z6GPgmpn9QEUJLo$aNOpf0V~lJ)i(PqVA13vo(UDYf6-g^9~0^%u$f;QTP(U z;t-l!D1l%UObF^TG%VIY*~5%_xIJNN&~FT_w^<89(@ktAfNX?wDHbp`et?!&QRk{6 z=#7K43^y@3Txo%YO$`6Te=P6k3eC7p74jTm067jZrpmVeBZfP{c)Z)AW7vRi4*SN~ zaah9mKVf75iTibcifqG~?F+j9ZW%XF!Z_S;chnOh+ z3r1T#x>9{fZw%q>k0Knt#Oyae^h1XvvmZ5JmvrbfS?Wx2PnlvoKgmYo30cZ)n-Y#x<#(S zf+2w5)mgH+3r%HZK4B8)TGGZ#M3QddEZrVP zN#xdJN*sZ-j4>+JZhmJYyH2wzvg7i!?UPqn z-Q-5wyfn!Mc6MjQ&VYtVly5C6EHccrB-+D_I@)uO!uKI*NToe9pk`-HQIB~?NNcJs zRE2rZmO4aIT^%c^D1|4-`iK{Gh#7T+0d;^Om3&)#TZ+fl*BlhlJD@7z{hie6q Sf1bVf>)+4+I%)P<&;A9#c~toT literal 0 HcmV?d00001 diff --git a/esp32_fw/data/numbers2-2.vlw b/esp32_fw/data/numbers2-2.vlw new file mode 100644 index 0000000000000000000000000000000000000000..60ade58bc1927eaa96096986cef2d06c4a01e5fc GIT binary patch literal 47218 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 literal 0 HcmV?d00001 diff --git a/esp32_fw/data/numbers3-1.vlw b/esp32_fw/data/numbers3-1.vlw new file mode 100644 index 0000000000000000000000000000000000000000..fff661e331a38f951da38cd5e25de3d779137da0 GIT binary patch literal 14076 zcmeH~+m2jC3`H9dFA)BKOAxOK_v_zw^-ks=}S|59*Ww)b|~r@ChbW;$V7s+7H# z?Q;2?ZcR?7(+8*1=|fw;*=nD!Y<*}vGvT>$F>@4_WQQl z=UZDX_ETHPd(Z6l`Oa3WS+Va;Z*@Mmg_`e}-9A6qYBfKx1%{eGns!w4Cu6N9&TBUE zex7-s+1_gE{eIanL*w%r9|*dG&0Ue%3vI4O6j%dF%zy*zCmit^Ks;KaKLXrG$1Zg4 z9hRBSV7US^+fG~tC^lO&mt;hG2!v$^Uu1JZVpBbiC^Q_BEGE{J zq=iKi@>ax}iAdf>j2HAiXP(|8vhI2Hz=o5V2NmZ%vNnOonb`#RD!6q|bMa%6!6%c@ z{R^9Bd}6zIY3;a5(im{n#2#3LXm%VW@%FU83lhDUEqO}44jgLD8c1RX?zv>$orxjI zLkOkh6;W@6#vNL|!qja`m%{w)Br`*Or8rkuk_w6YTLfhayq{Zg(QHa;h7YG~+wz-9 z6t!usy-UD~x({AL{7N0bi4SRiZ!e-%^QQG&bm%{-P|+O%+6eBLCLzP_vM20CClI|B{O@hs2D7yb=l!eXG z6T3~ie#s0IzIGcGgjrSfR(nh1DNE|oVWhK>C{7PeR@H?KlNAyYfl+ygElhYZ=7m=! zLV+S6@*J@6n+L1~QiXE?;GBibPi7>;hyh;UpaS_V1EfG*VFvWJ5Ug{6b}nGt3(-;m z(osOT~=BC4aYDFI=m80`Jh;d<0 zlHulssn;`!H@M-)A^OAf_m76NYE1k0A4PU0$cBZSzOS(P45%o1O~H@o;K#HdFF3U+ zKaOmb34(bMouY?6BrZB{?d0Bq;!Ya!xXZ%nCQM}3pmKUiOODj~fJk$J$iu1KQJ$$O z9#B$nflCi9KhW$G&%-F#@pLAQl4GaJD_U_$zIV(KS4Z0oyg9#37z;RQ#G+5r3TDxj zcAPuO7|kSk3WFZygO&#f9HyL+SQDHV)Y&j%!~&Zu0R(!qpc+b1{4~OZKxBvhE literal 0 HcmV?d00001 diff --git a/esp32_fw/data/numbers3-2.vlw b/esp32_fw/data/numbers3-2.vlw new file mode 100644 index 0000000000000000000000000000000000000000..1dbfa21b37c389e0e6156dd673a762805ba42e0f GIT binary patch literal 22861 zcmeI0S&L*v5QPgB1ogq6Ac!j{?km3Nzwk}*Wne(?|3e$OM8-L>+{)IP?waW;xN{>; ze0ef5?yZ_(PN&mbr_u0$>k?V80ev<2Tx!UJTxqg)EeYsk# z#c#^hK3~bz*bn8>b#F-AK3~h#JhR$2qBoyUh7#kUDtSHH9lI7q~8Ba7_%wvt<;@ku1I_?4>c{aV`LSYNMV@KSWD*~2=zH}X+ zI}0~?(Bcim;oP-1gg;!ij48>i#L*4x6X2m(c9HS$^u48R_A-30rF&@Dy^&aH$MpF+ zx-a;A3&d|jG(g43L?vk+h&Ano)g;>mmE#FTL*tG~(@YSMER897nHF;Z%Z1|L!A zS#W0L-bv*V$Pp&&olytuVz8^CfwPcQEAt)!k7NY7qyV`T1iIW0p_9XjmWyaBmLNfz zbcl>K2Z)*$$O(3>5Vfd#0itU6hQN4n7!U&ij|G78WkBZUg43}0HW!-(MlW_Q=$^*( zgOL)iqImw)_=xCx&xMu6DXo$Q5$TGS`@LYAyto4fuw!bvoq^`27oz%}XT2OXPaT#u zeTr)^OEFq0U{o?_ZploVnO0SrJtIIPO5qu#>B; zlpftuT|iKH?*`0Fd^Jq^+^;{qfWiU zsQL}^!yI|~rw%Rr-Hr4OvNxLk3dA8v{37OLp5YN;iDGPv3NBfk(>Ya&Ri91D%Pt?3 z6GPgmpn9QEUJLo$aNOpf0V~lJ)i(PqVA13vo(UDYf6-g^9~0^%u$f;QTP(U z;t-l!D1l%UObF^TG%VIY*~5%_xIJNN&~FT_w^<89(@ktAfNX?wDHbp`et?!&QRk{6 z=#7K43^y@3Txo%YO$`6Te=P6k3eC7p74jTm067jZrpmVeBZfP{c)Z)AW7vRi4*SN~ zaah9mKVf75iTibcifqG~?F+j9ZW%XF!Z_S;chnOh+ z3r1T#x>9{fZw%q>k0Knt#Oyae^h1XvvmZ5JmvrbfS?Wx2PnlvoKgmYo30cZ)n-Y#x<#(S zf+2w5)mgH+3r%HZK4B8)TGGZ#M3QddEZrVP zN#xdJN*sZ-j4>+JZhmJYyH2wzvg7i!?UPqn z-Q-5wyfn!Mc6MjQ&VYtVly5C6EHccrB-+D_I@)uO!uKI*NToe9pk`-HQIB~?NNcJs zRE2rZmO4aIT^%c^D1|4-`iK{Gh#7T+0d;^Om3&)#TZ+fl*BlhlJD@7z{hie6q Sf1bVf>)+4+I%)P<&;A9#c~toT literal 0 HcmV?d00001 diff --git a/esp32_fw/include/commstructs.h b/esp32_fw/include/commstructs.h index bf1f2a24..ac93dba7 100644 --- a/esp32_fw/include/commstructs.h +++ b/esp32_fw/include/commstructs.h @@ -56,4 +56,6 @@ struct pendingData { } __packed; #define BLOCK_DATA_SIZE 4096 -#define BLOCK_XFER_BUFFER_SIZE BLOCK_DATA_SIZE + sizeof(struct blockData) \ No newline at end of file +#define BLOCK_XFER_BUFFER_SIZE BLOCK_DATA_SIZE + sizeof(struct blockData) + +#pragma pack(pop) \ No newline at end of file diff --git a/esp32_fw/include/contentmanager.h b/esp32_fw/include/contentmanager.h new file mode 100644 index 00000000..64fe7a25 --- /dev/null +++ b/esp32_fw/include/contentmanager.h @@ -0,0 +1,15 @@ +#include + +#include +#include "makeimage.h" +#include +#include "tag_db.h" +#include + +void contentRunner(); +void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo); +bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin); +void drawDate(String &filename); +void drawNumber(String &filename, int32_t count, int32_t thresholdred); +bool getImgURL(String &filename, String URL, time_t fetched); +char *formatHttpDate(time_t t); \ No newline at end of file diff --git a/esp32_fw/include/makeimage.h b/esp32_fw/include/makeimage.h index 71d747ef..3ce9ef15 100644 --- a/esp32_fw/include/makeimage.h +++ b/esp32_fw/include/makeimage.h @@ -1,6 +1,8 @@ #include #include +#pragma once + struct BitmapFileHeader { uint8_t sig[2]; uint32_t fileSz; @@ -27,7 +29,6 @@ enum EinkClut { EinkClutThreeBlacksAndRed, }; -void tftinit(); void spr2grays(TFT_eSprite &spr, long w, long h, String fileout); void jpg2grays(String filein, String fileout); void bmp2grays(String filein, String fileout); diff --git a/esp32_fw/include/pendingdata.h b/esp32_fw/include/pendingdata.h index b4116bfb..1bbc1e9f 100644 --- a/esp32_fw/include/pendingdata.h +++ b/esp32_fw/include/pendingdata.h @@ -21,3 +21,5 @@ class pendingdata { void garbageCollection(void* parameter); extern std::vector pendingfiles; + +#pragma pack(pop) \ No newline at end of file diff --git a/esp32_fw/include/tag_db.h b/esp32_fw/include/tag_db.h index 2d44313a..3c6a77c2 100644 --- a/esp32_fw/include/tag_db.h +++ b/esp32_fw/include/tag_db.h @@ -1,4 +1,5 @@ #include +#include #include @@ -11,26 +12,36 @@ enum contentModes { CountDays, CountHours, Weather, - PubTrans, + Firmware, Memo, + ImageUrl, }; class tagRecord { - public: - tagRecord() : mac{0}, model(0), alias(""), lastseen(0), nextupdate(0), contentMode(Image), pending(false), button(false), currFilename(""), pendingFilename("") {} + public: + uint16_t nextCheckinpending; + tagRecord() : mac{0}, model(0), alias(""), lastseen(0), nextupdate(0), contentMode(Image), pending(false), button(false), md5{0}, md5pending{0}, CheckinInMinPending(0), expectedNextCheckin(0), modeConfigJson("") {} - uint8_t mac[6]; - u_int8_t model; - String alias; - uint32_t lastseen; - uint32_t nextupdate; - contentModes contentMode; - bool pending; - bool button; - String currFilename; - String pendingFilename; - static tagRecord* findByMAC(uint8_t mac[6]); + uint8_t mac[6]; + u_int8_t model; + String alias; + uint32_t lastseen; + uint32_t nextupdate; + contentModes contentMode; + bool pending; + bool button; + uint8_t md5[16]; + uint8_t md5pending[16]; + uint16_t CheckinInMinPending; + uint32_t expectedNextCheckin; + String modeConfigJson; + static tagRecord* findByMAC(uint8_t mac[6]); }; extern std::vector tagDB; -String tagDBtoJson(uint8_t mac[6] = nullptr); \ No newline at end of file +String tagDBtoJson(uint8_t mac[6] = nullptr, uint8_t startPos = 0); +void fillNode(JsonObject &tag, tagRecord* &taginfo); +void saveDB(String filename); +void loadDB(String filename); + +#pragma pack(pop) \ No newline at end of file diff --git a/esp32_fw/include/web.h b/esp32_fw/include/web.h index 71c40f5c..7badb987 100644 --- a/esp32_fw/include/web.h +++ b/esp32_fw/include/web.h @@ -1,11 +1,12 @@ -#include + +#include #include #include void init_web(); void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); -//extern void webSocketSendProcess(void *parameter); +extern void webSocketSendProcess(void *parameter); void wsString(String text); void wsSendTaginfo(uint8_t mac[6]); void wsSendSysteminfo(); diff --git a/esp32_fw/src/contentmanager.cpp b/esp32_fw/src/contentmanager.cpp new file mode 100644 index 00000000..1558159a --- /dev/null +++ b/esp32_fw/src/contentmanager.cpp @@ -0,0 +1,228 @@ +#include "contentmanager.h" + +#include +#include +#include +#include "newproto.h" +#include +#include +#include + +#include "commstructs.h" +#include "makeimage.h" +#include "web.h" + +void contentRunner() { + time_t now; + time(&now); + + for (int16_t c = 0; c < tagDB.size(); c++) { + tagRecord* taginfo = nullptr; + taginfo = tagDB.at(c); + + if (now >= taginfo->nextupdate || taginfo->button) { + uint8_t mac8[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + memcpy(mac8 + 2, taginfo->mac, 6); + uint8_t src[8]; + *((uint64_t *)src) = swap64(*((uint64_t *)mac8)); + + drawNew(src, taginfo->button, taginfo); + taginfo->button = false; + } + } +} + +void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { + time_t now; + time(&now); + struct tm *time_info = gmtime(&now); + + char buffer[64]; + uint8_t src[8]; + *((uint64_t *)src) = swap64(*((uint64_t *)mac)); + sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", src[2], src[3], src[4], src[5], src[6], src[7]); + String dst = (String)buffer; + + String filename = "/" + dst + ".bmp"; + + time_info->tm_hour = 0; + time_info->tm_min = 0; + time_info->tm_sec = 0; + time_info->tm_mday++; + time_t midnight = mktime(time_info); + + DynamicJsonDocument doc(500); + deserializeJson(doc, taginfo->modeConfigJson); + JsonObject cfgobj = doc.as(); + + switch (taginfo->contentMode) { + case Image: + + filename = cfgobj["filename"].as(); + if (filename && filename !="null" && !cfgobj["#fetched"].as()) { + if (prepareDataAvail(&filename, DATATYPE_IMGRAW, mac, cfgobj["timetolive"].as())) { + cfgobj["#fetched"] = true; + } else { + wsString("Error accessing " + filename); + } + taginfo->nextupdate = 3216153600; + } + break; + + case Today: + + drawDate(filename); + updateTagImage(filename, mac, (midnight - now) / 60 - 10); + taginfo->nextupdate = midnight; + break; + + case CountDays: + + if (buttonPressed) cfgobj["counter"] = 0; + drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"]); + updateTagImage(filename, mac, (midnight - now) / 60 - 5); + cfgobj["counter"] = (int32_t)cfgobj["counter"] + 1; + taginfo->nextupdate = midnight; + break; + + case CountHours: + + if (buttonPressed) cfgobj["counter"] = 0; + drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"]); + // updateTagImage(&filename, mac, (3600 - now % 3600) / 60); + // taginfo->nextupdate = now + 3600 - (now % 3600); + updateTagImage(filename, mac, 3); + cfgobj["counter"] = (int32_t)cfgobj["counter"] + 1; + taginfo->nextupdate = now + 300; + break; + + case Weather: + + // https://open-meteo.com/ + break; + + case Firmware: + + filename = cfgobj["filename"].as(); + if (filename && filename != "null" && !cfgobj["#fetched"].as()) { + if (prepareDataAvail(&filename, DATATYPE_UPDATE, mac, cfgobj["timetolive"].as())) { + cfgobj["#fetched"] = true; + } else { + wsString("Error accessing " + filename); + } + taginfo->nextupdate = 3216153600; + taginfo->contentMode = Image; + } + break; + + case Memo: + break; + case ImageUrl: + + if (getImgURL(filename, cfgobj["url"], (time_t)cfgobj["#fetched"])) { + updateTagImage(filename, mac, cfgobj["interval"].as()); + cfgobj["#fetched"] = now; + } + taginfo->nextupdate = now + 60 * cfgobj["interval"].as(); + break; + } + + taginfo->modeConfigJson = doc.as(); +} + +bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin) { + prepareDataAvail(&filename, DATATYPE_IMGRAW, dst, nextCheckin); + return true; +} + +void drawDate(String &filename) { + + TFT_eSPI tft = TFT_eSPI(); + TFT_eSprite spr = TFT_eSprite(&tft); + time_t now; + time(&now); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + String Dag[] = {"zondag","maandag","dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"}; + String Maand[] = {"januari", "februari", "maart", "april", "mei", "juni","juli", "augustus", "september", "oktober", "november", "december"}; + int weekday_number = timeinfo.tm_wday; + int month_number = timeinfo.tm_mon; + + LittleFS.begin(); + long w = 296, h = 128; // mag staand of liggend + spr.createSprite(w, h); + spr.setColorDepth(8); + spr.fillSprite(TFT_WHITE); + spr.setTextDatum(TC_DATUM); + spr.loadFont("calibrib62", LittleFS); + spr.setTextColor(TFT_RED, TFT_WHITE); + spr.drawString(Dag[timeinfo.tm_wday], w / 2, 10); + spr.loadFont("calibrib50", LittleFS); + spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.drawString(String(timeinfo.tm_mday) + " " + Maand[timeinfo.tm_mon], w / 2, 73); + spr.unloadFont(); + + spr2grays(spr, w, h, filename); + + spr.deleteSprite(); +} + +void drawNumber(String &filename, int32_t count, int32_t thresholdred) { + TFT_eSPI tft = TFT_eSPI(); + TFT_eSprite spr = TFT_eSprite(&tft); + + LittleFS.begin(); + long w = 296, h = 128; + spr.createSprite(w, h); + spr.setColorDepth(8); + spr.fillSprite(TFT_WHITE); + spr.setTextDatum(MC_DATUM); + if (count > thresholdred) { + spr.setTextColor(TFT_RED, TFT_WHITE); + } else { + spr.setTextColor(TFT_BLACK, TFT_WHITE); + } + String font = "numbers1-2"; + if (count>999) font="numbers2-2"; + if (count>9999) font="numbers3-2"; + spr.loadFont(font, LittleFS); + spr.drawString(String(count), w/2, h/2+10); + spr.unloadFont(); + + spr2grays(spr, w, h, filename); + + spr.deleteSprite(); +} + +bool getImgURL(String &filename, String URL, time_t fetched) { + // https://images.klari.net/kat-bw29.jpg + + LittleFS.begin(); + + Serial.println("get external " + URL); + HTTPClient http; + http.begin(URL); + http.addHeader("If-Modified-Since", formatHttpDate(fetched)); + http.setTimeout(5000); //timeout in ms + int httpCode = http.GET(); + if (httpCode == 200) { + File f = LittleFS.open(filename, "w"); + if (f) { + http.writeToStream(&f); + f.close(); + jpg2grays(filename, filename); + } + } else { + Serial.println("http " + String(httpCode)); + } + http.end(); + return (httpCode == 200); +} + +char *formatHttpDate(time_t t) { + static char buf[40]; + struct tm *timeinfo; + timeinfo = gmtime(&t); + strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", timeinfo); + return buf; +} diff --git a/esp32_fw/src/main.cpp b/esp32_fw/src/main.cpp index 71f04968..e46ea598 100644 --- a/esp32_fw/src/main.cpp +++ b/esp32_fw/src/main.cpp @@ -3,6 +3,7 @@ #include #include +#include "contentmanager.h" #include "flasher.h" #include "makeimage.h" #include "pendingdata.h" @@ -11,31 +12,35 @@ #include "tag_db.h" #include "web.h" -void freeHeapTask(void* parameter) { +void timeTask(void* parameter) { while (1) { - //Serial.printf("Free heap=%d\n", ESP.getFreeHeap()); - //time_t now; + time_t now; + time(&now); tm tm; if (!getLocalTime(&tm)) { Serial.println("Failed to obtain time"); + } else { + if (now % 10 == 0) wsSendSysteminfo(); + contentRunner(); } - wsSendSysteminfo(); - vTaskDelay(30000 / portTICK_PERIOD_MS); + vTaskDelay(1000 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); Serial.print(">\n"); - init_web(); configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org", "time.nist.gov"); // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv - xTaskCreate(freeHeapTask, "print free heap", 10000, NULL, 2, NULL); + init_web(); + loadDB("/tagDB.json"); + + xTaskCreate(timeTask, "timed tasks", 10000, NULL, 2, NULL); xTaskCreate(zbsRxTask, "zbsRX Process", 10000, NULL, 2, NULL); xTaskCreate(garbageCollection, "pending-data cleanup", 5000, NULL, 1, NULL); - //xTaskCreate(webSocketSendProcess, "ws", 5000, NULL,configMAX_PRIORITIES-10, NULL); + xTaskCreate(webSocketSendProcess, "ws", 5000, NULL,configMAX_PRIORITIES-10, NULL); } void loop() { diff --git a/esp32_fw/src/makeimage.cpp b/esp32_fw/src/makeimage.cpp index ff01570a..90cc66ee 100644 --- a/esp32_fw/src/makeimage.cpp +++ b/esp32_fw/src/makeimage.cpp @@ -8,28 +8,6 @@ TFT_eSPI tft = TFT_eSPI(); TFT_eSprite spr = TFT_eSprite(&tft); -void tftinit() { - //tijdelijk: voorbeeld voor aanmaak van plaatje - - LittleFS.begin(); - long w = 296, h = 128; // mag staand of liggend - spr.createSprite(w, h); - spr.setColorDepth(8); - spr.fillSprite(TFT_WHITE); - spr.setTextDatum(TC_DATUM); - spr.loadFont("calibrib62", LittleFS); - spr.setTextColor(TFT_RED, TFT_WHITE); - spr.drawString("zondag", w / 2, 10); - spr.loadFont("calibrib50", LittleFS); - spr.setTextColor(TFT_BLACK, TFT_WHITE); - spr.drawString("29 januari", w / 2, 73); - spr.unloadFont(); - - spr2grays(spr, w, h, "/testspr3.bmp"); - - spr.deleteSprite(); -} - bool spr_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { spr.pushImage(x, y, w, h, bitmap); return 1; @@ -40,6 +18,7 @@ void jpg2grays(String filein, String fileout) { TJpgDec.setCallback(spr_output); uint16_t w = 0, h = 0; TJpgDec.getFsJpgSize(&w, &h, filein); + Serial.println("jpeg conversion " + String(w) + "x" + String(h)); spr.createSprite(w, h); spr.setColorDepth(8); diff --git a/esp32_fw/src/newproto.cpp b/esp32_fw/src/newproto.cpp index 64f48fd7..297dd2b1 100644 --- a/esp32_fw/src/newproto.cpp +++ b/esp32_fw/src/newproto.cpp @@ -1,4 +1,3 @@ -#pragma pack(push, 1) #include "newproto.h" #include @@ -51,6 +50,12 @@ void prepareCancelPending(uint64_t ver) { } bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t nextCheckin) { + + if (nextCheckin > 1440) { + //to prevent very long sleeps of the tag + nextCheckin = 0; + } + *filename = "/" + *filename; if (!LittleFS.exists(*filename)) return false; fs::File file = LittleFS.open(*filename); @@ -101,6 +106,20 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t md5.getBytes(md5bytes); } + uint8_t src[8]; + *((uint64_t*)src) = swap64(*((uint64_t*)dst)); + uint8_t mac[6]; + memcpy(mac, src + 2, sizeof(mac)); + tagRecord* taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo != nullptr) { + if (memcmp(md5bytes, taginfo->md5pending, 16) == 0) { + wsString("new image is the same as current image. not updating tag."); + wsSendTaginfo(mac); + return false; + } + } + // the message that will be sent to the AP to tell the tag there is data pending struct pendingData pending = {0}; memcpy(pending.targetMac, dst, 8); @@ -124,21 +143,23 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t pendinginfo->timeout = 1800; pendingfiles.push_back(pendinginfo); - char dst_path[64]; - sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); - file = LittleFS.open(dst_path, "w"); - int bytes_written = file.write(pendinginfo->data, pendinginfo->len); - file.close(); + if (dataType != DATATYPE_UPDATE) { + char dst_path[64]; + sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); + file = LittleFS.open(dst_path, "w"); + int bytes_written = file.write(pendinginfo->data, pendinginfo->len); + file.close(); - uint8_t src[8]; - *((uint64_t*)src) = swap64(*((uint64_t*)dst)); - uint8_t mac[6]; - memcpy(mac, src + 2, sizeof(mac)); - tagRecord* taginfo = nullptr; - taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { - taginfo->pending = true; + wsString("new image pending: " + String(dst_path)); + if (taginfo != nullptr) { + taginfo->pending = true; + taginfo->CheckinInMinPending = nextCheckin + 1; + memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes)); + } + } else { + Serial.println("firmware upload pending"); } + wsSendTaginfo(mac); return true; @@ -196,23 +217,30 @@ void processXferComplete(struct espXferComplete* xfc) { uint8_t mac[6]; memcpy(mac, src + 2, sizeof(mac)); - tagRecord* taginfo = nullptr; - taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { - taginfo->pending = false; - } - wsSendTaginfo(mac); - char src_path[64]; char dst_path[64]; char tmp_path[64]; sprintf(src_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", src[2], src[3], src[4], src[5], src[6], src[7]); sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.bmp\0", src[2], src[3], src[4], src[5], src[6], src[7]); sprintf(tmp_path, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", src[2], src[3], src[4], src[5], src[6], src[7]); + if (LittleFS.exists(dst_path)) { + LittleFS.remove(dst_path); + } LittleFS.rename(src_path, dst_path); if (LittleFS.exists(tmp_path)) { LittleFS.remove(tmp_path); } + + time_t now; + time(&now); + tagRecord* taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo != nullptr) { + taginfo->pending = false; + taginfo->expectedNextCheckin = now + 60 * taginfo->CheckinInMinPending + 30; + memcpy(taginfo->md5, taginfo->md5pending, sizeof(taginfo->md5pending)); + } + wsSendTaginfo(mac); } void processDataReq(struct espAvailDataReq* eadr) { @@ -225,19 +253,20 @@ void processDataReq(struct espAvailDataReq* eadr) { memcpy(mac, src + 2, sizeof(mac)); taginfo = tagRecord::findByMAC(mac); - time_t now; - time(&now); if (taginfo == nullptr) { taginfo = new tagRecord; memcpy(taginfo->mac, src + 2, sizeof(taginfo->mac)); taginfo->pending = false; tagDB.push_back(taginfo); } + time_t now; + time(&now); taginfo->lastseen = now; + taginfo->expectedNextCheckin = now + 300; taginfo->button = (eadr->adr.buttonState == 1); sprintf(buffer, " #include #include diff --git a/esp32_fw/src/tag_db.cpp b/esp32_fw/src/tag_db.cpp index 162ec82c..e1af5f24 100644 --- a/esp32_fw/src/tag_db.cpp +++ b/esp32_fw/src/tag_db.cpp @@ -1,9 +1,12 @@ -#include - #include "tag_db.h" + +#include #include + #include +#include "LittleFS.h" + std::vector tagDB; tagRecord* tagRecord::findByMAC(uint8_t mac[6]) { @@ -17,35 +20,149 @@ tagRecord* tagRecord::findByMAC(uint8_t mac[6]) { return nullptr; } -String tagDBtoJson(uint8_t mac[6]) { - DynamicJsonDocument doc(1000); +String tagDBtoJson(uint8_t mac[6], uint8_t startPos) { + DynamicJsonDocument doc(2500); JsonArray tags = doc.createNestedArray("tags"); - for (int16_t c = 0; c < tagDB.size(); c++) { + for (int16_t c = startPos; c < tagDB.size(); c++) { tagRecord* taginfo = nullptr; taginfo = tagDB.at(c); - bool select = false; - if (mac) { + bool select = false; + if (mac) { if (memcmp(taginfo->mac, mac, 6) == 0) { select = true; } } else { - select = true; - } - if (select) { - JsonObject tag = tags.createNestedObject(); - char buffer[64]; - sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", taginfo->mac[0], taginfo->mac[1], taginfo->mac[2], taginfo->mac[3], taginfo->mac[4], taginfo->mac[5]); - tag["mac"] = (String)buffer; - tag["lastseen"] = taginfo->lastseen; - tag["nextupdate"] = taginfo->nextupdate; - tag["model"] = taginfo->model; - tag["pending"] = taginfo->pending; - tag["button"] = taginfo->button; - tag["alias"] = taginfo->alias; - tag["contentmode"] = taginfo->contentMode; + select = true; + } + if (select) { + JsonObject tag = tags.createNestedObject(); + fillNode(tag, taginfo); + if (mac) { + break; + } + } + if (doc.capacity()-doc.memoryUsage() < doc.memoryUsage()/(c+1) + 100) { + doc["continu"] = c+1; + break; } } return doc.as(); -} \ No newline at end of file +} + +void fillNode(JsonObject &tag, tagRecord* &taginfo) { + char buffer[16]; + sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", taginfo->mac[0], taginfo->mac[1], taginfo->mac[2], taginfo->mac[3], taginfo->mac[4], taginfo->mac[5]); + tag["mac"] = (String)buffer; + char hex[7]; + sprintf(hex, "%02x%02x%02x\0", taginfo->md5[0], taginfo->md5[1], taginfo->md5[2]); + tag["hash"] = hex; + tag["lastseen"] = taginfo->lastseen; + tag["nextupdate"] = taginfo->nextupdate; + tag["nextcheckin"] = taginfo->expectedNextCheckin; + tag["model"] = taginfo->model; + tag["pending"] = taginfo->pending; + tag["button"] = taginfo->button; + tag["alias"] = taginfo->alias; + tag["contentmode"] = taginfo->contentMode; + tag["modecfgjson"] = taginfo->modeConfigJson; +} + +void saveDB(String filename) { + DynamicJsonDocument doc(2500); + + Serial.println("start writing DB to file"); + long t = millis(); + + LittleFS.begin(); + fs::File file = LittleFS.open(filename, "w"); + if (!file) { + Serial.println("saveDB: Failed to open file"); + return; + } + + file.write('['); + + for (int16_t c = 0; c < tagDB.size(); c++) { + doc.clear(); + tagRecord* taginfo = nullptr; + taginfo = tagDB.at(c); + + JsonObject tag = doc.createNestedObject(); + fillNode(tag, taginfo); + if (c > 0) { + file.write(','); + } + serializeJson(doc, file); + } + file.write(']'); + + file.close(); + Serial.println(millis() - t); + Serial.println("finished writing file"); + + return; +} + +void loadDB(String filename) { + StaticJsonDocument<400> doc; + + Serial.println("start reading DB from file"); + long t = millis(); + + LittleFS.begin(); + fs::File readfile = LittleFS.open(filename, "r"); + if (!readfile) { + Serial.println("loadDB: Failed to open file"); + return; + } + + time_t now; + time(&now); + bool parsing = true; + + if (readfile.find("[")) { + while (parsing) { + DeserializationError err = deserializeJson(doc, readfile); + if (!err) { + JsonObject tag = doc[0]; + String dst = tag["mac"].as(); + uint8_t mac[12]; + if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) == 6) { + tagRecord* taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo == nullptr) { + taginfo = new tagRecord; + memcpy(taginfo->mac, mac, sizeof(taginfo->mac)); + tagDB.push_back(taginfo); + } + //taginfo->lastseen = (uint32_t)tag["lastseen"]; + taginfo->lastseen = 0; + taginfo->nextupdate = (uint32_t)tag["nextupdate"]; + taginfo->expectedNextCheckin = (uint16_t)tag["nextcheckin"]; + if (taginfo->expectedNextCheckin < now - 1800) { + taginfo->expectedNextCheckin = now + 1800; + } + taginfo->model = (uint8_t)tag["model"]; + taginfo->pending = false; + taginfo->button = false; + taginfo->alias = tag["alias"].as(); + taginfo->contentMode = static_cast(tag["contentmode"]); + taginfo->modeConfigJson = tag["modecfgjson"].as(); + } + } else { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(err.c_str()); + parsing = false; + } + parsing = parsing && readfile.find(","); + } + } + + readfile.close(); + Serial.println(millis() - t); + Serial.println("finished reading file"); + + return; +} diff --git a/esp32_fw/src/web.cpp b/esp32_fw/src/web.cpp index 3eb35563..5b8618a3 100644 --- a/esp32_fw/src/web.cpp +++ b/esp32_fw/src/web.cpp @@ -2,6 +2,7 @@ #include #include + #include #include #include @@ -54,62 +55,15 @@ void webSocketSendProcess(void *parameter) { // sendStatus(STATUS_WIFI_ACTIVITY); DynamicJsonDocument doc(1500); if (ulNotificationValue & 2) { // WS_SEND_MODE_STATUS) { - /* doc["rxActive"] = status.rxActive; - doc["txActive"] = status.txActive; - doc["freq"] = status.freq; - doc["txMode"] = status.currentmode; - */ } /* JsonArray statusframes = doc.createNestedArray("frames"); - for (uint8_t c = 0; c < STATUSFRAMELISTSIZE; c++) { - if (statusframearr[c]) { - JsonObject statusframe = statusframes.createNestedObject(); - statusframe["frame"] = statusframearr[c]->frameno; - statusframe["isTX"] = statusframearr[c]->isTX; - statusframe["freq"] = statusframearr[c]->freq; - statusframe["txSkipped"] = statusframearr[c]->txCancelled; - switch (statusframearr[c]->rxtype) { - case flexsynctype::SYNC_FLEX_1600: - statusframe["rxType"] = "FLEX_1600"; - break; - case flexsynctype::SYNC_FLEX_3200_2: - statusframe["rxType"] = "FLEX_3200_2"; - break; - case flexsynctype::SYNC_FLEX_3200_4: - statusframe["rxType"] = "FLEX_3200_4"; - break; - case flexsynctype::SYNC_FLEX_6400: - statusframe["rxType"] = "FLEX_3200_4"; - break; - default: - break; - } - switch (statusframearr[c]->txformat) { - case txframe::FORMAT_FLEX: - statusframe["txType"] = "FLEX"; - break; - case txframe::FORMAT_POCSAG: - statusframe["txType"] = "POCSAG"; - break; - case txframe::FORMAT_IDLE: - statusframe["txType"] = "IDLE"; - break; - case txframe::FORMAT_BLOCKED: - statusframe["txType"] = "BLOCKED"; - break; - default: - break; - } - } - } }*/ size_t len = measureJson(doc); xSemaphoreTake(wsMutex, portMAX_DELAY); auto buffer = std::make_shared>(len); serializeJson(doc, buffer->data(), len); // ws.textAll((char*)buffer->data()); - //ws.textAll("ohai"); xSemaphoreGive(wsMutex); } } @@ -214,28 +168,14 @@ void wsSendSysteminfo() { } void wsSendTaginfo(uint8_t mac[6]) { - DynamicJsonDocument doc(1000); - JsonArray tags = doc.createNestedArray("tags"); - JsonObject tag = tags.createNestedObject(); - char buffer[64]; - sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - tag["mac"] = (String)buffer; - tagRecord *taginfo = nullptr; - taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { - tag["lastseen"] = taginfo->lastseen; - tag["nextupdate"] = taginfo->nextupdate; - tag["model"] = taginfo->model; - tag["pending"] = taginfo->pending; - tag["button"] = taginfo->button; - tag["alias"] = taginfo->alias; - tag["contentmode"] = taginfo->contentMode; - } + String json = ""; + json = tagDBtoJson(mac); xSemaphoreTake(wsMutex, portMAX_DELAY); - ws.textAll(doc.as()); + ws.textAll(json); xSemaphoreGive(wsMutex); + } void init_web() { @@ -282,70 +222,6 @@ void init_web() { }, doImageUpload); - server.on("/send_image", HTTP_POST, [](AsyncWebServerRequest *request) { - String filename; - String dst; - uint16_t nextCheckin; - if (request->hasParam("filename", true) && request->hasParam("dst", true)) { - filename = request->getParam("filename", true)->value(); - dst = request->getParam("dst", true)->value(); - nextCheckin = request->getParam("ttl",true)->value().toInt(); - uint8_t mac_addr[12]; // I expected this to return like 8 values, but if I make the array 8 bytes long, things die. - mac_addr[0] = 0x00; - mac_addr[1] = 0x00; - if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", - &mac_addr[2], - &mac_addr[3], - &mac_addr[4], - &mac_addr[5], - &mac_addr[6], - &mac_addr[7]) != 6) { - request->send(200, "text/plain", "Something went wrong trying to parse the mac address"); - } else { - *((uint64_t *)mac_addr) = swap64(*((uint64_t *)mac_addr)); - if (prepareDataAvail(&filename, DATATYPE_IMGRAW, mac_addr, nextCheckin)) { - request->send(200, "text/plain", "Sending to " + dst); - } else { - request->send(200, "text/plain", "Couldn't find filename :("); - } - } - return; - } - request->send(200, "text/plain", "Didn't get the required filename + dst"); - return; - }); - - server.on("/send_fw", HTTP_POST, [](AsyncWebServerRequest *request) { - String filename; - String dst; - if (request->hasParam("filename", true) && request->hasParam("dst", true)) { - filename = request->getParam("filename", true)->value(); - dst = request->getParam("dst", true)->value(); - uint8_t mac_addr[12]; // I expected this to return like 8 values, but if I make the array 8 bytes long, things die. - mac_addr[0] = 0x00; - mac_addr[1] = 0x00; - if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", - &mac_addr[2], - &mac_addr[3], - &mac_addr[4], - &mac_addr[5], - &mac_addr[6], - &mac_addr[7]) != 6) { - request->send(200, "text/plain", "Something went wrong trying to parse the mac address"); - } else { - *((uint64_t *)mac_addr) = swap64(*((uint64_t *)mac_addr)); - if (prepareDataAvail(&filename, DATATYPE_UPDATE, mac_addr, 0)) { - request->send(200, "text/plain", "Sending FW to " + dst); - } else { - request->send(200, "text/plain", "Couldn't find filename :("); - } - } - return; - } - request->send(200, "text/plain", "Didn't get the required filename + dst"); - return; - }); - server.on("/req_checkin", HTTP_POST, [](AsyncWebServerRequest *request) { String filename; String dst; @@ -383,8 +259,12 @@ void init_web() { json = tagDBtoJson(mac); } } else { - json = tagDBtoJson(); - } + uint8_t startPos=0; + if (request->hasParam("pos")) { + startPos = atoi(request->getParam("pos")->value().c_str()); + } + json = tagDBtoJson(nullptr,startPos); + } request->send(200, "application/json", json); }); @@ -397,9 +277,12 @@ void init_web() { taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { taginfo->alias = request->getParam("alias", true)->value(); + taginfo->modeConfigJson = request->getParam("modecfgjson", true)->value(); taginfo->contentMode = (contentModes)atoi(request->getParam("contentmode", true)->value().c_str()); taginfo->model = atoi(request->getParam("model", true)->value().c_str()); + taginfo->nextupdate = 0; wsSendTaginfo(mac); + saveDB("/tagDB.json"); request->send(200, "text/plain", "Ok, saved"); } else { request->send(200, "text/plain", "Error while saving: mac not found"); @@ -414,55 +297,6 @@ void init_web() { request->send(200, "text/html", "-"); return; } - Serial.printf("NOT_FOUND: "); - - switch (request->method()) { - case HTTP_GET: - Serial.printf("GET"); - break; - case HTTP_POST: - Serial.printf("POST"); - break; - case HTTP_DELETE: - Serial.printf("DELETE"); - break; - case HTTP_PUT: - Serial.printf("PUT"); - break; - case HTTP_PATCH: - Serial.printf("PATCH"); - break; - case HTTP_HEAD: - Serial.printf("HEAD"); - break; - case HTTP_OPTIONS: - Serial.printf("OPTIONS"); - break; - - default: - Serial.printf("UNKNOWN"); - break; - } - Serial.printf(" http://%s%s\n", request->host().c_str(), request->url().c_str()); - - if (request->contentLength()) { - Serial.printf("_CONTENT_TYPE: %s\n", request->contentType().c_str()); - Serial.printf("_CONTENT_LENGTH: %u\n", request->contentLength()); - } - for (int i = 0; i < request->headers(); i++) { - AsyncWebHeader *h = request->getHeader(i); - Serial.printf("_HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str()); - } - for (int i = 0; i < request->params(); i++) { - AsyncWebParameter *p = request->getParam(i); - if (p->isFile()) { - Serial.printf("_FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size()); - } else if (p->isPost()) { - Serial.printf("_POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); - } else { - Serial.printf("_GET[%s]: %s\n", p->name().c_str(), p->value().c_str()); - } - } request->send(404); });