Merge pull request #9 from nlimper/development

spiffseditor improvements
This commit is contained in:
Jelmer
2023-02-04 11:47:43 +01:00
committed by GitHub
35 changed files with 1178 additions and 516 deletions

BIN
esp32_fw/data/alignment.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

777
esp32_fw/data/www/edit.html Normal file
View File

@@ -0,0 +1,777 @@
<!--This is the plain html source of the hex encoded Editor-Page embedded in SPIFFSEditor.cpp -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>ESP Editor</title>
<link rel="icon" href="data:,">
<style type="text/css" media="screen">
.cm {
z-index: 300;
position: absolute;
left: 5px;
border: 1px solid #444;
background-color: #F5F5F5;
display: none;
box-shadow: 0 0 10px rgba(0, 0, 0, .4);
font-size: 12px;
font-family: sans-serif;
font-weight: bold;
}
.cm ul {
list-style: none;
top: 0;
left: 0;
margin: 0;
padding: 0;
}
.cm li {
position: relative;
min-width: 60px;
cursor: pointer;
}
.cm span {
color: #444;
display: inline-block;
padding: 6px;
}
.cm li:hover {
background: #444;
}
.cm li:hover span {
color: #EEE;
}
.tvu ul,
.tvu li {
padding: 0;
margin: 0;
list-style: none;
}
.tvu li {
display: flex;
justify-content: stretch;
}
.tvu input {
position: absolute;
opacity: 0;
}
.tvu {
font: normal 12px Verdana, Arial, Sans-serif;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
color: #444;
line-height: 16px;
}
.tvu .icon {
padding: 0 0 0 5px;
cursor: pointer;
display: inline-block;
width: 20px;
text-align: center;
font-size: 1.1em;
}
.tvu .filename {
padding: 2px 0 0 2px;
cursor: pointer;
display: inline-block;
width: auto;
flex: 2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tvu .size {
padding: 2px 5px 0 5px;
cursor: pointer;
display: inline-block;
text-align: right;
}
.tvu li:hover {
background-color:#cccccc;
}
#uploader {
position: absolute;
top: 0;
right: 0;
left: 0;
background-color: #444;
color: #EEE;
height: 45px;
}
button {
background-color: #eeeeee;
border: 0px;
cursor: pointer;
padding: 3px 5px;
margin: 10px;
appearance: none;
}
input {
background: #ffffff;
border: 0px;
appearance: none;
padding: 2px 5px;
margin: 10px;
}
input[type="file"] {
background: none;
}
.uploadpath {
background: none;
color: white;
}
#tree {
position: absolute;
top: 50px;
bottom: 0;
left: 0;
width: 300px;
}
#editor,
#preview {
position: absolute;
top: 45px;
right: 0;
bottom: 0;
left: 300px;
border-left: 1px solid #EEE;
}
#preview {
background-color: #EEE;
padding: 5px;
}
#loader {
position: absolute;
top: 36%;
right: 40%;
}
.loader {
z-index: 10000;
border: 8px solid #b5b5b5;
/* Grey */
border-top: 8px solid #3498db;
/* Blue */
border-bottom: 8px solid #3498db;
/* Blue */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite;
display: none;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<script>
if (typeof XMLHttpRequest === "undefined") {
XMLHttpRequest = function () {
try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e) { }
try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e) { }
try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { }
throw new Error("This browser does not support XMLHttpRequest.");
};
}
function ge(a) {
return document.getElementById(a);
}
function ce(a) {
return document.createElement(a);
}
function sortByKey(array, key) {
return array.sort(function (a, b) {
var x = a[key]; var y = b[key];
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});
}
var QueuedRequester = function () {
this.queue = [];
this.running = false;
this.xmlhttp = null;
}
QueuedRequester.prototype = {
_request: function (req) {
this.running = true;
if (!req instanceof Object) return;
var that = this;
function ajaxCb(x, d) {
return function () {
if (x.readyState == 4) {
ge("loader").style.display = "none";
d.callback(x.status, x.responseText);
if (that.queue.length === 0) that.running = false;
if (that.running) that._request(that.queue.shift());
}
}
}
ge("loader").style.display = "block";
var p = "";
if (req.params instanceof FormData) {
p = req.params;
} else if (req.params instanceof Object) {
for (var key in req.params) {
if (p === "")
p += (req.method === "GET") ? "?" : "";
else
p += "&";
p += encodeURIComponent(key) + "=" + encodeURIComponent(req.params[key]);
};
}
this.xmlhttp = new XMLHttpRequest();
this.xmlhttp.onreadystatechange = ajaxCb(this.xmlhttp, req);
if (req.method === "GET") {
this.xmlhttp.open(req.method, req.url + p, true);
this.xmlhttp.send();
} else {
this.xmlhttp.open(req.method, req.url, true);
if (p instanceof String)
this.xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
this.xmlhttp.send(p);
}
},
stop: function () {
if (this.running) this.running = false;
if (this.xmlhttp && this.xmlhttp.readyState < 4) {
this.xmlhttp.abort();
}
},
add: function (method, url, params, callback) {
this.queue.push({ url: url, method: method, params: params, callback: callback });
if (!this.running) {
this._request(this.queue.shift());
}
}
}
var requests = new QueuedRequester();
function createFileUploader(element, tree, editor) {
var xmlHttp;
var refresh = ce("button");
refresh.innerHTML = 'Refresh List';
ge(element).appendChild(refresh);
var path = ce("input");
path.id = "upload-path";
path.type = "text";
path.name = "path";
path.classList.add("uploadpath");
path.defaultValue = "/";
ge(element).appendChild(path);
var input = ce("input");
input.type = "file";
input.multiple = false;
input.name = "data";
input.id = "upload-select";
ge(element).appendChild(input);
/*
var mkfile = ce("button");
mkfile.innerHTML = 'Create';
ge(element).appendChild(mkfile);
*/
var filename = ce("input");
filename.id = "editor-filename";
filename.type = "text";
filename.size = 20;
ge(element).appendChild(filename);
var button = ce("button");
button.id = "button";
button.innerHTML = 'Upload';
button.style.display = 'none';
ge(element).appendChild(button);
var savefile = ce("button");
savefile.id = "savefile";
savefile.innerHTML = ' Save ';
savefile.style.display = 'none';
ge(element).appendChild(savefile);
function httpPostProcessRequest(status, responseText) {
if (status != 200)
alert("ERROR[" + status + "]: " + responseText);
else
tree.refreshPath(path.value);
}
function createPath(p) {
var formData = new FormData();
formData.append("path", p);
requests.add("PUT", "/edit", formData, httpPostProcessRequest);
}
/*
mkfile.onclick = function (e) {
createPath(filename.value);
editor.loadUrl(filename.value);
path.value = (filename.value);
};
*/
savefile.onclick = function (e) {
editor.execCommand('saveCommand');
};
refresh.onclick = function (e) {
tree.refreshPath(path.value);
};
button.onclick = function (e) {
if (input.files.length === 0) {
return;
}
var formData = new FormData();
formData.append("data", input.files[0], ge("editor-filename").value);
requests.add("POST", "/edit", formData, httpPostProcessRequest);
var uploadSelect = ge("upload-select");
uploadSelect.value = "";
};
input.onchange = function (e) {
if (input.files.length === 0) return;
var filename = input.files[0].name;
ge("editor-filename").value = path.value + (path.value=="/" ? "" : "/") + filename;
button.style.display = 'inline-block';
savefile.style.display = 'none';
};
}
function createTree(element, editor) {
var preview = ge("preview");
var treeRoot = ce("div");
treeRoot.className = "tvu";
ge(element).appendChild(treeRoot);
function loadDownload(path) {
ge('download-frame').src = "/edit?download=" + path;
}
function loadPreview(path) {
ge("button").style.display = 'none';
ge("savefile").style.display = 'none';
var edfname = ge("editor-filename");
var filename = path;
var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
var name = /(.*)\.[^.]+$/.exec(filename)[1];
if (typeof name !== undefined) {
filename = name;
}
edfname.value = filename + "." + ext;
ge("editor").style.display = "none";
preview.style.display = "block";
preview.innerHTML = '<img src="/edit?edit=' + path + '&_cb=' + Date.now() + '" style="max-width:100%; max-height:100%; margin:auto; display:block;" />';
}
function fillFileMenu(el, path) {
var list = ce("ul");
el.appendChild(list);
var action = ce("li");
list.appendChild(action);
if (isImageFile(path)) {
action.innerHTML = "<span>Preview</span>";
action.onclick = function (e) {
loadPreview(path);
if (document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
};
} else if (isTextFile(path)) {
action.innerHTML = "<span>Edit</span>";
action.onclick = function (e) {
editor.loadUrl(path);
if (document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
};
}
var download = ce("li");
list.appendChild(download);
download.innerHTML = "<span>Download</span>";
download.onclick = function (e) {
loadDownload(path);
if (document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
};
var delFile = ce("li");
list.appendChild(delFile);
delFile.innerHTML = "<span>Delete</span>";
delFile.onclick = function (e) {
httpDelete(path);
if (document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(el);
};
}
function showContextMenu(event, path, isfile) {
var divContext = ce("div");
var scrollTop = document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
var scrollLeft = document.body.scrollLeft ? document.body.scrollLeft : document.documentElement.scrollLeft;
var left = event.clientX + scrollLeft;
var top = event.clientY + scrollTop;
divContext.className = 'cm';
divContext.style.display = 'block';
divContext.style.left = left + 'px';
divContext.style.top = top + 'px';
fillFileMenu(divContext, path);
document.body.appendChild(divContext);
var width = divContext.offsetWidth;
var height = divContext.offsetHeight;
divContext.onmouseout = function (e) {
if (e.clientX < left || e.clientX > (left + width) || e.clientY < top || e.clientY > (top + height)) {
if (document.body.getElementsByClassName('cm').length > 0) document.body.removeChild(divContext);
}
};
}
function createTreeLeaf(path, name, size) {
var leaf = ce("li");
leaf.id = path + (path != "/" ? "/":"") + name;
var icon = ce("span");
icon.innerHTML = "&#128196;";
icon.classList.add("icon");
leaf.appendChild(icon);
var label = ce("span");
label.innerHTML = name;
label.classList.add("filename");
leaf.appendChild(label);
var filesize = ce("span");
filesize.innerHTML = formatBytes(size);
filesize.classList.add("size");
leaf.appendChild(filesize);
leaf.onclick = function (e) {
if (isTextFile(leaf.id.toLowerCase())) {
editor.loadUrl(leaf.id);
} else if (isImageFile(leaf.id.toLowerCase())) {
loadPreview(leaf.id);
}
};
leaf.oncontextmenu = function (e) {
e.preventDefault();
e.stopPropagation();
showContextMenu(e, leaf.id, true);
};
return leaf;
}
function createDirLeaf(path, name, size) {
var leaf = ce("li");
leaf.id = path + (path != "/" ? "/" : "") + name;
var icon = ce("span");
icon.innerHTML = "&#128193;";
icon.classList.add("icon");
leaf.appendChild(icon);
var label = ce("span");
label.innerHTML = name;
label.classList.add("filename");
leaf.appendChild(label);
var filesize = ce("span");
filesize.innerHTML = "";
filesize.classList.add("size");
leaf.appendChild(filesize);
leaf.onclick = function (e) {
treeRoot.removeChild(treeRoot.childNodes[0]);
if (name == "..") {
httpGet(treeRoot, "/");
} else {
httpGet(treeRoot, "/" + name);
}
};
leaf.oncontextmenu = function (e) {
e.preventDefault();
e.stopPropagation();
showContextMenu(e, leaf.id, true);
};
return leaf;
}
function formatBytes(bytes, decimalPlaces = 2) {
if (bytes === 0) return '';
const k = 1024;
const dm = decimalPlaces < 0 ? 0 : decimalPlaces;
const sizes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function addList(parent, path, items) {
ge("upload-path").value = path
ge("editor-filename").value = path
sortByKey(items, 'name');
var list = ce("ul");
parent.appendChild(list);
if (path != "/") {
list.appendChild(createDirLeaf("/", "..", 0));
}
var ll = items.length;
for (var i = 0; i < ll; i++) {
if (items[i].type === "file") {
list.appendChild(createTreeLeaf(path, items[i].name, items[i].size));
}
if (items[i].type === "dir") {
list.insertBefore(createDirLeaf(path, items[i].name, items[i].size), list.firstChild);
}
}
}
function isTextFile(path) {
var ext = /(?:\.([^.]+))?$/.exec(path)[1];
if (typeof ext !== undefined) {
switch (ext) {
case "txt":
case "htm":
case "html":
case "js":
case "css":
case "xml":
case "json":
case "conf":
case "ini":
case "h":
case "c":
case "cpp":
case "php":
case "hex":
case "ino":
case "pde":
return true;
}
}
return false;
}
function isImageFile(path) {
var ext = /(?:\.([^.]+))?$/.exec(path)[1];
if (typeof ext !== undefined) {
switch (ext) {
case "png":
case "jpg":
case "gif":
case "bmp":
return true;
}
}
return false;
}
this.refreshPath = function (path) {
treeRoot.removeChild(treeRoot.childNodes[0]);
httpGet(treeRoot, path);
};
function delCb(path) {
return function (status, responseText) {
if (status != 200) {
alert("ERROR[" + status + "]: " + responseText);
} else {
treeRoot.removeChild(treeRoot.childNodes[0]);
httpGet(treeRoot, "/");
}
}
}
function httpDelete(filename) {
var formData = new FormData();
formData.append("path", filename);
requests.add("DELETE", "/edit", formData, delCb(filename));
}
function getCb(parent, path) {
return function (status, responseText) {
if (status == 200)
addList(parent, path, JSON.parse(responseText));
}
}
function httpGet(parent, path) {
requests.add("GET", "/edit", { list: path }, getCb(parent, path));
}
httpGet(treeRoot, "/");
return this;
}
function createEditor(element, file, lang, theme, type) {
function getLangFromFilename(filename) {
var lang = "plain";
var ext = /(?:\.([^.]+))?$/.exec(filename)[1];
if (typeof ext !== undefined) {
switch (ext) {
case "txt": lang = "plain"; break;
case "hex": lang = "plain"; break;
case "conf": lang = "plain"; break;
case "htm": lang = "html"; break;
case "js": lang = "javascript"; break;
case "h": lang = "c_cpp"; break;
case "c": lang = "c_cpp"; break;
case "cpp": lang = "c_cpp"; break;
case "css":
case "scss":
case "php":
case "html":
case "json":
case "xml":
case "ini": lang = ext;
}
}
return lang;
}
if (typeof file === "undefined") file = "/index.html";
if (typeof lang === "undefined") {
lang = getLangFromFilename(file);
}
if (typeof theme === "undefined") theme = "textmate";
if (typeof type === "undefined") {
type = "text/" + lang;
if (lang === "c_cpp") type = "text/plain";
}
var editor = ace.edit(element);
function httpPostProcessRequest(status, responseText) {
if (status != 200) alert("ERROR[" + status + "]: " + responseText);
}
function httpPost(filename, data, type) {
var formData = new FormData();
formData.append("data", new Blob([data], { type: type }), filename);
requests.add("POST", "/edit", formData, httpPostProcessRequest);
}
function httpGetProcessRequest(status, responseText) {
ge("preview").style.display = "none";
ge("editor").style.display = "block";
if (status == 200)
editor.setValue(responseText);
else
editor.setValue("");
editor.clearSelection();
}
function httpGet(theUrl) {
requests.add("GET", "/edit", { edit: theUrl }, httpGetProcessRequest);
}
if (lang !== "plain") editor.getSession().setMode("ace/mode/" + lang);
editor.setTheme("ace/theme/" + theme);
editor.$blockScrolling = Infinity;
editor.getSession().setUseSoftTabs(true);
editor.getSession().setTabSize(2);
editor.setHighlightActiveLine(true);
editor.setShowPrintMargin(false);
editor.commands.addCommand({
name: 'saveCommand',
bindKey: { win: 'Ctrl-S', mac: 'Command-S' },
exec: function (editor) {
httpPost(file, editor.getValue() + "", type);
},
readOnly: false
});
editor.commands.addCommand({
name: 'undoCommand',
bindKey: { win: 'Ctrl-Z', mac: 'Command-Z' },
exec: function (editor) {
editor.getSession().getUndoManager().undo(false);
},
readOnly: false
});
editor.commands.addCommand({
name: 'redoCommand',
bindKey: { win: 'Ctrl-Shift-Z', mac: 'Command-Shift-Z' },
exec: function (editor) {
editor.getSession().getUndoManager().redo(false);
},
readOnly: false
});
editor.loadUrl = function (filename) {
ge("button").style.display = 'none';
ge("savefile").style.display = 'inline-block';
var edfname = ge("editor-filename");
edfname.value = filename;
file = filename;
lang = getLangFromFilename(file);
type = "text/" + lang;
if (lang !== "plain") editor.getSession().setMode("ace/mode/" + lang);
httpGet(file);
};
return editor;
}
function onBodyLoad() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { vars[key] = value; });
var editor = createEditor("editor", vars.file, vars.lang, vars.theme);
var tree = createTree("tree", editor);
createFileUploader("uploader", tree, editor);
if (typeof vars.file === "undefined") vars.file = "/index.htm";
editor.loadUrl(vars.file);
};
</script>
<script id='ace' src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"
charset="utf-8"></script>
<script>
if (typeof ace.edit == "undefined") {
var script = document.createElement('script');
script.src = "/ace.js";
script.async = false;
document.head.appendChild(script);
}
</script>
</head>
<body onload="onBodyLoad();">
<div id="loader" class="loader"></div>
<div id="uploader"></div>
<div id="tree"></div>
<div id="editor"></div>
<div id="preview" style="display:none;"></div>
<iframe id=download-frame style='display:none;'></iframe>
</body>
</html>

View File

@@ -7,6 +7,7 @@
<title>Solum - alternative proto AP</title>
<link rel="stylesheet" href="main.css" type="text/css" />
<link rel="icon" href="data:,">
</head>
<body>
@@ -24,10 +25,10 @@
<p>
<label for="cfgmodel">Model</label>
<select id="cfgmodel">
<option value="0">unknown</option>
<option value="1">1.54" 152x152px</option>
<option value="2">2.9" 296x128px</option>
<option value="3">4.2" 400x300px</option>
<option value="99">unknown</option>
<option value="0">1.54" 152x152px</option>
<option value="1">2.9" 296x128px</option>
<option value="2">4.2" 400x300px</option>
</select>
</p>
<p>
@@ -37,8 +38,8 @@
<option value="1">current date</option>
<option value="2">count days</option>
<option value="3">count hours</option>
<option value="4" disabled>current weather</option>
<option value="6" disabled>memo text</option>
<option value="4">current weather</option>
<option value="6">memo text</option>
<option value="7">image url</option>
<option value="5">firmware update</option>
</select>
@@ -58,6 +59,7 @@
<div class="actionbox">
<div>
<div class="editbtn"><a href="/edit" target="littlefs" class="filebutton">edit littleFS</a></div>
Currently active tags:<br>
</div>
</div>

View File

@@ -65,16 +65,21 @@ label {
flex-wrap: wrap;
}
.filebutton {
padding:2px 5px;
background-color: #cccccc;
text-decoration: none;
color: black;
}
.editbtn {
float:right;
}
.columns div {
flex: 1;
}
.actionbox div div {
padding: 5px;
margin: 5px;
background-color: white;
}
input {
border: solid 1px #666666;
padding: 4px;
@@ -239,6 +244,9 @@ ul.messages li.new {
animation-timing-function: ease-in-out;
}
.error {
color: red;
}
@media(max-width: 460px) {

View File

@@ -1,7 +1,7 @@
const $ = document.querySelector.bind(document);
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 models = ["1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"];
const contentModeOptions = [];
contentModeOptions[0] = ["filename","timetolive"];
contentModeOptions[1] = [];
@@ -42,7 +42,10 @@ function connect() {
console.log(event.data);
const msg = JSON.parse(event.data);
if (msg.logMsg) {
showMessage(msg.logMsg);
showMessage(msg.logMsg,false);
}
if (msg.errMsg) {
showMessage(msg.errMsg,true);
}
if (msg.tags) {
processTags(msg.tags);
@@ -226,11 +229,15 @@ function contentselected() {
});
}
function showMessage(message) {
function showMessage(message,iserr) {
const messages = $('#messages');
var date = new Date(),
time = date.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit'});
messages.insertAdjacentHTML("afterbegin", '<li class="new">'+htmlEncode(time+' '+message)+'</li>');
if (iserr) {
messages.insertAdjacentHTML("afterbegin", '<li class="new error">' + htmlEncode(time + ' ' + message) + '</li>');
} else {
messages.insertAdjacentHTML("afterbegin", '<li class="new">'+htmlEncode(time+' '+message)+'</li>');
}
}
function htmlEncode(input) {

View File

@@ -9,7 +9,11 @@
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);
void drawDate(String &filename, tagRecord *&taginfo);
void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord *&taginfo);
void drawWeather(String &filename, String location, tagRecord *&taginfo);
bool getImgURL(String &filename, String URL, time_t fetched);
char *formatHttpDate(time_t t);
char *formatHttpDate(time_t t);
String urlEncode(const char *msg);
int windSpeedToBeaufort(float windSpeed);
String windDirectionIcon(int degrees);

View File

@@ -24,6 +24,9 @@
// this determines how long images will be cached;
#define PENDING_DATA_TIMEOUT 60
#define SOLUM_154_033 0
#define SOLUM_29_033 1
#define SOLUM_42_033 2
// flasher options
#define CUSTOM_MAC_HDR 0x0000

View File

@@ -7,7 +7,8 @@ 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);
void wsString(String text);
void wsLog(String text);
void wsErr(String text);
void wsSendTaginfo(uint8_t mac[6]);
void wsSendSysteminfo();

View File

@@ -2,396 +2,9 @@
#include <FS.h>
// File: edit.htm.gz, Size: 4151
#define edit_htm_gz_len 4151
const uint8_t edit_htm_gz[] PROGMEM = {
0x1F, 0x8B, 0x08, 0x08, 0xB8, 0x94, 0xB1, 0x59, 0x00, 0x03, 0x65, 0x64, 0x69, 0x74, 0x2E, 0x68,
0x74, 0x6D, 0x00, 0xB5, 0x3A, 0x0B, 0x7B, 0xDA, 0xB8, 0xB2, 0x7F, 0xC5, 0x71, 0xCF, 0x66, 0xED,
0x83, 0x31, 0x90, 0xA4, 0xD9, 0xD6, 0xC4, 0xC9, 0x42, 0x92, 0x36, 0x6D, 0xF3, 0x6A, 0x80, 0xB6,
0x69, 0x4F, 0xEE, 0x7E, 0xC2, 0x16, 0xA0, 0xC6, 0x96, 0x5D, 0x5B, 0x0E, 0x49, 0x59, 0xFE, 0xFB,
0x9D, 0x91, 0x6C, 0xB0, 0x09, 0x69, 0x77, 0xCF, 0xBD, 0xBB, 0xDD, 0x2D, 0x92, 0x46, 0x33, 0x9A,
0x19, 0xCD, 0x53, 0xDE, 0xBD, 0x8D, 0xA3, 0x8B, 0xC3, 0xFE, 0xF5, 0xE5, 0xB1, 0x36, 0x11, 0x61,
0xB0, 0xBF, 0x87, 0x7F, 0x6B, 0x01, 0xE1, 0x63, 0x97, 0xF2, 0xFD, 0x3D, 0xC1, 0x44, 0x40, 0xF7,
0x8F, 0x7B, 0x97, 0xDA, 0xB1, 0xCF, 0x44, 0x94, 0xEC, 0x35, 0xD4, 0xCA, 0x5E, 0x2A, 0x1E, 0x02,
0xAA, 0x85, 0xD4, 0x67, 0xC4, 0x4D, 0xBD, 0x84, 0xC2, 0x66, 0xDB, 0x0B, 0x67, 0xDF, 0xEB, 0x8C,
0xFB, 0xF4, 0xDE, 0xD9, 0x6E, 0x36, 0xDB, 0x71, 0x94, 0x32, 0xC1, 0x22, 0xEE, 0x90, 0x61, 0x1A,
0x05, 0x99, 0xA0, 0xED, 0x80, 0x8E, 0x84, 0xF3, 0x3C, 0xBE, 0x6F, 0x0F, 0xA3, 0xC4, 0xA7, 0x89,
0xD3, 0x8A, 0xEF, 0x35, 0x00, 0x31, 0x5F, 0x7B, 0xB6, 0xB3, 0xB3, 0xD3, 0x1E, 0x12, 0xEF, 0x76,
0x9C, 0x44, 0x19, 0xF7, 0xEB, 0x5E, 0x14, 0x44, 0x89, 0xF3, 0x6C, 0xF4, 0x1C, 0xFF, 0xB4, 0x7D,
0x96, 0xC6, 0x01, 0x79, 0x70, 0x78, 0xC4, 0x29, 0xE0, 0xDE, 0xD7, 0xD3, 0x09, 0xF1, 0xA3, 0xA9,
0xD3, 0xD4, 0x9A, 0x5A, 0xAB, 0x09, 0x44, 0x92, 0xF1, 0x90, 0x18, 0x4D, 0x0B, 0xFF, 0xD8, 0x3B,
0x66, 0x7B, 0x14, 0x71, 0x51, 0x4F, 0xD9, 0x77, 0xEA, 0xB4, 0xB6, 0xE0, 0x34, 0x39, 0x1D, 0x91,
0x90, 0x05, 0x0F, 0x4E, 0x4A, 0x78, 0x5A, 0x4F, 0x69, 0xC2, 0x46, 0x6A, 0x79, 0x4A, 0xD9, 0x78,
0x22, 0x9C, 0xDF, 0x9A, 0xCD, 0x39, 0xF0, 0xAF, 0x65, 0xC1, 0x2C, 0x60, 0x29, 0x20, 0xA3, 0x78,
0xEA, 0x3C, 0x11, 0xC5, 0x4E, 0x53, 0xB1, 0xDE, 0x6C, 0x87, 0x24, 0x19, 0x33, 0x0E, 0x83, 0x98,
0xF8, 0x3E, 0xE3, 0x63, 0x47, 0xA1, 0x05, 0x6C, 0xB6, 0x90, 0x36, 0xA1, 0x01, 0x11, 0xEC, 0x8E,
0xB6, 0x43, 0xC6, 0xEB, 0x53, 0xE6, 0x8B, 0x89, 0xB3, 0x0B, 0x3C, 0xB6, 0xBD, 0x2C, 0x49, 0x41,
0xA6, 0x38, 0x62, 0x5C, 0xD0, 0x44, 0xA2, 0xA5, 0x31, 0xE1, 0xB3, 0x5C, 0x54, 0x54, 0x40, 0x21,
0x27, 0xE3, 0x01, 0xE3, 0xB4, 0x3E, 0x0C, 0x22, 0xEF, 0x76, 0x71, 0xD2, 0x6E, 0x7C, 0x9F, 0x9F,
0xE5, 0x4C, 0xA2, 0x3B, 0x9A, 0xCC, 0x96, 0xEA, 0x92, 0xD8, 0x15, 0x60, 0x85, 0x34, 0xA5, 0x74,
0x6E, 0x8B, 0xBB, 0x0C, 0xA0, 0x96, 0xFC, 0x05, 0x29, 0x17, 0xFC, 0x2F, 0x45, 0x5A, 0x11, 0x5C,
0xA1, 0x30, 0x1E, 0x67, 0x62, 0xF6, 0xF8, 0x2A, 0xA3, 0x98, 0x78, 0x4C, 0x3C, 0xA0, 0xFC, 0xB0,
0x6D, 0x86, 0xBA, 0x04, 0xAC, 0x24, 0x24, 0x81, 0x86, 0x3A, 0xD7, 0x3E, 0xD0, 0xC4, 0x27, 0x9C,
0x58, 0x9D, 0x84, 0x91, 0xC0, 0xEA, 0x2D, 0xB5, 0x5E, 0x0F, 0xA3, 0xEF, 0xF5, 0x0C, 0xC6, 0x30,
0x0F, 0xA8, 0x27, 0x94, 0x92, 0xE1, 0x1E, 0x86, 0xB7, 0x4C, 0x3C, 0x06, 0x3C, 0x5A, 0x28, 0xA9,
0x4B, 0x2A, 0x69, 0xA2, 0x2E, 0xB0, 0x25, 0xD5, 0x83, 0x1C, 0x4B, 0xC9, 0x95, 0x50, 0xF5, 0x61,
0x24, 0x44, 0x14, 0x4A, 0x93, 0x5B, 0x08, 0xAC, 0x49, 0xAB, 0x79, 0xF1, 0xE8, 0x46, 0xD6, 0x6B,
0xBF, 0x44, 0xBE, 0x0D, 0x7A, 0x15, 0xCC, 0x23, 0x41, 0x9D, 0x04, 0x6C, 0xCC, 0x9D, 0x90, 0xF9,
0x7E, 0x40, 0x4B, 0x56, 0xEB, 0x64, 0x49, 0x60, 0xF8, 0x44, 0x10, 0x87, 0x85, 0x64, 0x4C, 0x1B,
0x31, 0x1F, 0x03, 0x34, 0xA5, 0xBB, 0x3B, 0x16, 0xFB, 0xD0, 0xBD, 0xB8, 0x9A, 0x36, 0xDF, 0xBD,
0x1E, 0x47, 0x1D, 0xF8, 0xE7, 0xBC, 0x37, 0x98, 0x1C, 0x0F, 0xC6, 0x30, 0xEA, 0xE2, 0xB4, 0xF3,
0xFE, 0xB0, 0xF3, 0x1E, 0x7E, 0x0E, 0x5B, 0xB5, 0xAF, 0xA3, 0x6F, 0xB8, 0xD0, 0x7D, 0xED, 0x77,
0xFB, 0x83, 0xE3, 0x4E, 0xE7, 0x5D, 0xE3, 0xCD, 0xF9, 0xF4, 0xE3, 0xBB, 0x5D, 0x04, 0x77, 0x83,
0xE6, 0xD5, 0x87, 0x49, 0x73, 0xB0, 0xF5, 0x32, 0xF4, 0x4F, 0xFC, 0x89, 0x17, 0x0E, 0x3A, 0xEF,
0x3F, 0x5E, 0xDD, 0x5D, 0x87, 0x83, 0x71, 0xEF, 0x63, 0x6B, 0xF2, 0x79, 0xEB, 0x43, 0xEF, 0xF3,
0xC7, 0x57, 0xB7, 0xF4, 0xD3, 0xC9, 0xDB, 0xCF, 0xFD, 0x29, 0x20, 0x1C, 0x45, 0xBD, 0xC1, 0x55,
0xF7, 0x43, 0x77, 0xFC, 0xB9, 0xEB, 0x1D, 0xDF, 0x0F, 0x83, 0xF3, 0xEE, 0xEB, 0xCE, 0xB0, 0xB3,
0xE5, 0x51, 0x3A, 0xEE, 0x5F, 0x75, 0xB3, 0x37, 0xEF, 0x2E, 0xC6, 0x8C, 0x4D, 0x7A, 0x9F, 0xCF,
0xFB, 0xDE, 0xE1, 0xF3, 0xD3, 0xC1, 0x49, 0x87, 0x4D, 0xCE, 0xDF, 0x5E, 0x35, 0x6F, 0x5F, 0xBF,
0x3B, 0x3C, 0xF2, 0xAE, 0xDF, 0x5E, 0xEF, 0x1E, 0x6D, 0x37, 0x7E, 0xFB, 0xED, 0xCC, 0xBF, 0x60,
0xBC, 0x7F, 0xF7, 0xBD, 0x33, 0x3E, 0x9C, 0xBE, 0x78, 0x48, 0xFB, 0x93, 0x37, 0x77, 0xBC, 0xF1,
0x21, 0xFA, 0xFA, 0xE6, 0xE1, 0x0C, 0xFE, 0xBB, 0xBC, 0xAC, 0x0D, 0x7B, 0xAD, 0x74, 0xF0, 0xFE,
0xCD, 0x87, 0xAD, 0xF4, 0xE5, 0xF3, 0xB8, 0x7B, 0x74, 0x74, 0x17, 0x0E, 0x2F, 0x1B, 0xA1, 0x7F,
0x3B, 0x12, 0x2F, 0xB6, 0x45, 0x7C, 0x3D, 0xCE, 0x3E, 0x7F, 0x7B, 0xFE, 0x76, 0xD2, 0xB8, 0xA0,
0xE4, 0x7A, 0x52, 0x7B, 0xF8, 0xFE, 0xF0, 0x62, 0xD2, 0x3F, 0xB9, 0x3B, 0x0F, 0xC8, 0xFD, 0xF9,
0xB9, 0xF7, 0x3D, 0xAC, 0x05, 0xE4, 0xE5, 0x45, 0x3F, 0x20, 0x49, 0x6B, 0xE0, 0x77, 0x1A, 0xB5,
0xC3, 0xAD, 0xCE, 0x8E, 0x48, 0xAE, 0x0E, 0xF9, 0xD1, 0xF6, 0xD7, 0xDE, 0x8B, 0x6E, 0xB7, 0x15,
0x0D, 0xBF, 0x6D, 0xBD, 0xBE, 0xDD, 0x7D, 0x3D, 0xD8, 0x7D, 0x3F, 0x7C, 0xDF, 0xE9, 0xED, 0x74,
0x07, 0xE4, 0xBA, 0xF7, 0xBE, 0x33, 0xDA, 0x19, 0x4E, 0x26, 0xEF, 0xDE, 0xF5, 0x5F, 0xF9, 0x9D,
0xEF, 0x49, 0xE7, 0x62, 0xDA, 0xB9, 0x3F, 0x1E, 0x74, 0x4E, 0x6A, 0xEF, 0x8E, 0xCF, 0x9A, 0xAD,
0xDE, 0xF5, 0xF6, 0xF8, 0x6C, 0x77, 0xDA, 0x4D, 0x8F, 0x3B, 0xEF, 0xBB, 0xCD, 0xF1, 0xDB, 0x5A,
0x48, 0x3E, 0x47, 0x87, 0xDB, 0xE3, 0x37, 0xBB, 0xEC, 0xF2, 0x9A, 0x74, 0xDE, 0x74, 0xDF, 0xA6,
0xEC, 0x2A, 0x3C, 0x19, 0x34, 0x3B, 0x9D, 0xD3, 0x0B, 0xFA, 0xEA, 0x70, 0x9B, 0xBC, 0xDB, 0xF2,
0x3E, 0x82, 0xFE, 0x07, 0x9F, 0xE8, 0x6F, 0xB5, 0xCE, 0xF4, 0xA2, 0x19, 0x78, 0x2F, 0x69, 0xFF,
0xE4, 0xBA, 0x2F, 0x6F, 0xE7, 0x38, 0x78, 0xD5, 0xBF, 0xED, 0x65, 0xEF, 0xC3, 0xC3, 0x43, 0x53,
0xE3, 0x51, 0x3D, 0xA1, 0x31, 0x25, 0xA2, 0x1C, 0xAE, 0x16, 0xFE, 0x01, 0xB6, 0xB5, 0xB4, 0xC2,
0xDC, 0x4F, 0x05, 0xBD, 0x17, 0x75, 0x9F, 0x7A, 0x51, 0x42, 0xE4, 0x1E, 0x40, 0xA0, 0x09, 0x9A,
0xD8, 0xFC, 0x77, 0x19, 0x3F, 0x35, 0x15, 0x3F, 0x35, 0xC2, 0x7D, 0xCD, 0x28, 0x1C, 0x01, 0x83,
0x87, 0x4F, 0xEF, 0x98, 0x47, 0xEB, 0x31, 0xBB, 0xA7, 0x41, 0x5D, 0x22, 0x3B, 0x4D, 0x73, 0x26,
0xFD, 0xAD, 0xD8, 0x46, 0x38, 0x98, 0x9A, 0xA4, 0x5A, 0x2C, 0xF8, 0x5F, 0x89, 0x47, 0x21, 0xB0,
0x81, 0xCB, 0x84, 0xF8, 0xAB, 0x7C, 0x27, 0x4A, 0xEA, 0xC3, 0x6C, 0x3C, 0x62, 0xF7, 0xE0, 0xD0,
0x23, 0xC6, 0x99, 0xA0, 0x5A, 0x2B, 0x9D, 0xFF, 0x5E, 0x90, 0xB9, 0xA5, 0x0F, 0xA3, 0x84, 0x84,
0x34, 0xD5, 0xFE, 0x22, 0x99, 0xD9, 0x28, 0x89, 0xC2, 0x65, 0x10, 0x99, 0x8B, 0xA8, 0x34, 0x99,
0xCF, 0x9F, 0x65, 0x71, 0x10, 0x11, 0x10, 0x73, 0x4D, 0xE4, 0x50, 0xF1, 0x34, 0x91, 0x6E, 0xB5,
0x88, 0xAB, 0xB9, 0x9B, 0x6D, 0xA1, 0x5B, 0x96, 0xDD, 0x7A, 0x6B, 0x67, 0xE9, 0xBA, 0x75, 0xB9,
0x17, 0xE3, 0xFD, 0x9A, 0x4C, 0x81, 0xF1, 0xA0, 0x14, 0xEE, 0x9E, 0x09, 0x50, 0xE9, 0x13, 0x87,
0xCB, 0x43, 0xF2, 0xC8, 0xB0, 0x60, 0x40, 0x05, 0xEA, 0x96, 0x8C, 0xD4, 0x85, 0x24, 0xB0, 0x6F,
0xFE, 0x8C, 0xCA, 0xBC, 0x67, 0x3D, 0x8B, 0x13, 0xB8, 0x0D, 0x3A, 0xFD, 0x11, 0xCD, 0x42, 0xA6,
0x2A, 0x6D, 0x45, 0x53, 0x65, 0xBC, 0x5C, 0x84, 0x65, 0xDA, 0x93, 0xBC, 0x16, 0xA4, 0x1F, 0x4B,
0x05, 0xE0, 0x05, 0x37, 0xCF, 0x91, 0x9B, 0x1F, 0x6A, 0x75, 0x7B, 0xF7, 0x97, 0x9C, 0x87, 0x9D,
0xE6, 0x2F, 0x73, 0x3B, 0xDF, 0x5B, 0xA4, 0xE4, 0x56, 0x13, 0xFE, 0x29, 0x32, 0xEF, 0x8B, 0x25,
0x0B, 0xC3, 0xE7, 0xF8, 0xA7, 0x60, 0x10, 0xE9, 0x94, 0x80, 0xDB, 0x3B, 0x2F, 0x5F, 0xF8, 0xC3,
0x02, 0x98, 0x0B, 0xF6, 0x24, 0x3C, 0x21, 0x3E, 0xCB, 0x52, 0xE7, 0x79, 0xF3, 0x97, 0x5C, 0x9F,
0x5B, 0x3B, 0x28, 0xFB, 0xE2, 0x2E, 0x71, 0xB2, 0xB4, 0xD8, 0x34, 0x66, 0x5C, 0xDB, 0x4A, 0x35,
0xBC, 0x6F, 0x92, 0x2C, 0x0C, 0xB3, 0x92, 0xED, 0xE7, 0xBF, 0x2F, 0x4D, 0x13, 0xF7, 0xCF, 0x9A,
0xBF, 0xCC, 0x44, 0x02, 0xD9, 0x64, 0x04, 0xB9, 0xC6, 0x49, 0x22, 0x41, 0x04, 0x35, 0x9A, 0xE6,
0x1C, 0x84, 0x5B, 0x03, 0xD8, 0xDE, 0x6D, 0xFA, 0x74, 0x6C, 0xCE, 0xE7, 0x7B, 0x0D, 0x99, 0xD7,
0xA0, 0x6C, 0xF1, 0x12, 0x16, 0x8B, 0xFD, 0x51, 0xC6, 0x3D, 0xE4, 0x41, 0x1B, 0x53, 0x83, 0x9A,
0xB3, 0x84, 0x8A, 0x2C, 0xE1, 0x9A, 0x1F, 0x79, 0x19, 0x1A, 0xBB, 0x3D, 0xA6, 0xE2, 0x58, 0xD9,
0x7D, 0xF7, 0xE1, 0x8D, 0x0F, 0x3B, 0xE6, 0x0B, 0x04, 0x6F, 0x2D, 0x02, 0x38, 0x30, 0x9C, 0x97,
0xE3, 0x54, 0xF6, 0x43, 0x82, 0x01, 0x22, 0xEF, 0xE8, 0x83, 0x41, 0x2D, 0xB1, 0x40, 0xA4, 0x36,
0xAE, 0x1B, 0xC5, 0x2E, 0x80, 0x71, 0x73, 0x76, 0x07, 0x4A, 0x20, 0x2E, 0xFD, 0x22, 0x6E, 0x2C,
0xE6, 0x72, 0xF8, 0x69, 0xE7, 0xBB, 0xC9, 0x1E, 0x3B, 0xA8, 0xB7, 0x1C, 0xB2, 0xCF, 0x0E, 0x5A,
0xE0, 0x5E, 0x65, 0x6E, 0xE4, 0xB9, 0xAF, 0x58, 0x40, 0x07, 0xB9, 0xC3, 0xE1, 0x31, 0x48, 0x6C,
0xB1, 0x85, 0x28, 0xE2, 0x5B, 0xCD, 0xE6, 0x86, 0x4B, 0x0F, 0x48, 0x00, 0x39, 0xCC, 0xD0, 0x8F,
0xAF, 0xAE, 0x2E, 0xAE, 0xBE, 0xE8, 0x35, 0x5A, 0xD3, 0x6F, 0x1C, 0x4D, 0xAF, 0x71, 0xD3, 0x11,
0x76, 0x42, 0x47, 0x09, 0x4D, 0x27, 0x97, 0x44, 0x4C, 0x8C, 0xD4, 0xBE, 0x23, 0x41, 0x56, 0x16,
0x84, 0xA1, 0xDC, 0xC8, 0xA2, 0x70, 0x39, 0x9D, 0x6A, 0xAF, 0x40, 0xCD, 0x47, 0x90, 0xEA, 0xDA,
0xC2, 0x26, 0x71, 0x4C, 0xB9, 0x6F, 0xE8, 0x31, 0x20, 0xEA, 0x16, 0x35, 0xAD, 0x84, 0x7E, 0xCB,
0x68, 0x2A, 0x52, 0x1B, 0x2C, 0xD7, 0xD0, 0x2F, 0x07, 0x7D, 0xDD, 0xD2, 0x1B, 0xE8, 0x47, 0x3A,
0xF0, 0x46, 0xCC, 0x39, 0x52, 0x89, 0x5C, 0xD0, 0xA4, 0x3E, 0xCC, 0xC0, 0xA0, 0xB8, 0x6E, 0xB6,
0x23, 0x9B, 0x71, 0x4E, 0x93, 0x93, 0xFE, 0xD9, 0xA9, 0xAB, 0x5F, 0x29, 0x46, 0xB4, 0x53, 0x28,
0x48, 0x74, 0x4B, 0x5E, 0x51, 0x7E, 0xC8, 0xE1, 0x84, 0x05, 0xBE, 0x11, 0x99, 0x6D, 0x24, 0xE1,
0x49, 0x12, 0xB2, 0x40, 0x01, 0x0A, 0x9E, 0x2D, 0x1E, 0x62, 0xEA, 0xEA, 0x23, 0x50, 0x86, 0x6E,
0x79, 0x76, 0x98, 0x05, 0x82, 0xC5, 0x01, 0x75, 0x37, 0x5A, 0x30, 0xE3, 0x60, 0x41, 0xAE, 0x8E,
0xB9, 0x19, 0x61, 0xCC, 0x77, 0x75, 0x15, 0xA1, 0xF2, 0xB8, 0xB6, 0xEE, 0x14, 0x4F, 0x9D, 0x92,
0x56, 0x4E, 0x49, 0xCB, 0xB8, 0x4A, 0xE0, 0x34, 0x3F, 0x18, 0xC3, 0x3C, 0xCE, 0xD4, 0x51, 0x05,
0xCC, 0xA7, 0x23, 0x02, 0x9C, 0x7C, 0x40, 0x6D, 0xBA, 0x7A, 0x63, 0xDD, 0x41, 0xA9, 0x3A, 0xC8,
0xAF, 0x6A, 0xC4, 0x2F, 0x6B, 0x44, 0xDD, 0xEE, 0x3A, 0x64, 0x5F, 0x21, 0x07, 0x55, 0xE4, 0xA0,
0x8C, 0x7C, 0x28, 0x8D, 0x64, 0x1D, 0x72, 0xA0, 0x90, 0x93, 0x8A, 0x88, 0x89, 0x14, 0x51, 0x85,
0xBD, 0x3A, 0x6A, 0x13, 0x05, 0xD2, 0xAD, 0xA4, 0x22, 0x66, 0x62, 0x83, 0x97, 0x92, 0x61, 0x40,
0x7D, 0x77, 0xA3, 0x09, 0x33, 0x2C, 0xB6, 0xDD, 0xAD, 0xE6, 0x9A, 0x33, 0x12, 0x75, 0x46, 0x56,
0x65, 0x30, 0x2B, 0x33, 0xA8, 0xF5, 0xC8, 0x1D, 0xD5, 0xD6, 0x31, 0x98, 0x99, 0x56, 0x60, 0x47,
0xDC, 0x0B, 0x98, 0x77, 0xEB, 0x2E, 0xBD, 0xC5, 0x9C, 0xB1, 0x85, 0x85, 0x5A, 0x5C, 0x06, 0xBA,
0x01, 0x94, 0x5E, 0x8B, 0xA5, 0x7C, 0x80, 0xFA, 0x9E, 0x5B, 0xD9, 0x5A, 0x02, 0xDC, 0xA6, 0xF7,
0xD4, 0x3B, 0x8C, 0xC2, 0x90, 0xA0, 0xED, 0xA6, 0xC0, 0x41, 0x3E, 0xD1, 0xCD, 0xB9, 0x15, 0xAD,
0xC5, 0x79, 0xC2, 0x45, 0x2C, 0x7F, 0x3D, 0x8B, 0x23, 0x03, 0x5C, 0xCE, 0xF5, 0x6C, 0xD4, 0x61,
0x6A, 0x83, 0x1E, 0xC7, 0x62, 0xF2, 0x13, 0x17, 0x2A, 0x0C, 0x54, 0xA2, 0x7C, 0x69, 0xDE, 0x58,
0x0B, 0x91, 0x56, 0x7C, 0xEA, 0xA2, 0xB7, 0xE2, 0x54, 0xA8, 0xBC, 0x8A, 0x5D, 0x9A, 0x4B, 0x1D,
0x94, 0x61, 0xB9, 0xBD, 0x2F, 0xA0, 0xFA, 0x7C, 0x0E, 0xE7, 0x01, 0xFF, 0x13, 0x68, 0xF9, 0xE8,
0x5F, 0x17, 0x60, 0xC9, 0xA3, 0x34, 0x78, 0x8B, 0xBB, 0x0D, 0xE3, 0xC0, 0xF9, 0x8F, 0x6D, 0x7C,
0xF9, 0x1F, 0xFB, 0xA6, 0x66, 0x9A, 0x07, 0xFF, 0x6A, 0x48, 0x0D, 0x1B, 0xC2, 0xFC, 0xD2, 0xBA,
0xB1, 0x08, 0x80, 0xED, 0x7F, 0x9B, 0xFF, 0xB1, 0x25, 0xB8, 0x02, 0x6B, 0xDF, 0x45, 0x90, 0x49,
0xF0, 0x24, 0x34, 0xB0, 0x68, 0xA4, 0x91, 0xCD, 0x4D, 0x43, 0xB8, 0xA4, 0x72, 0x8D, 0x35, 0x51,
0xD3, 0x6D, 0x88, 0x53, 0x50, 0x5B, 0xAC, 0x04, 0xBF, 0x3E, 0x24, 0x7A, 0x15, 0x5B, 0x17, 0x00,
0xC9, 0x3D, 0xCA, 0x0C, 0x3D, 0x22, 0x97, 0x52, 0xCB, 0x0C, 0x02, 0x42, 0xA7, 0x89, 0xE7, 0x2A,
0xAD, 0x1D, 0x14, 0x30, 0x17, 0xA2, 0xE0, 0xBC, 0x1C, 0x2D, 0x15, 0xEA, 0xAA, 0xFD, 0x17, 0x0A,
0xA3, 0xD6, 0x12, 0x8A, 0x04, 0x31, 0xAD, 0xD8, 0x79, 0xC6, 0x72, 0x75, 0x4C, 0x59, 0xBA, 0x35,
0x59, 0x5D, 0x96, 0xAD, 0x04, 0xAE, 0x2F, 0x8D, 0xFE, 0xD7, 0x3D, 0x16, 0x8E, 0xB5, 0x12, 0x3F,
0xF8, 0x97, 0xFB, 0x2B, 0x46, 0xE4, 0xCD, 0x3F, 0xBC, 0x21, 0x70, 0x05, 0xA6, 0x41, 0x6D, 0x1E,
0x4D, 0x0D, 0xB3, 0xF6, 0xAB, 0xAE, 0x49, 0x8A, 0xAE, 0x1E, 0x92, 0xFB, 0xBC, 0xA7, 0xC4, 0x8C,
0xD7, 0xD6, 0x70, 0x5E, 0xB4, 0x28, 0xF9, 0x82, 0xEC, 0xE6, 0x48, 0x26, 0xA2, 0xB6, 0x56, 0x64,
0x52, 0xD5, 0xCA, 0xE8, 0x5A, 0x63, 0xFF, 0xD7, 0x4A, 0x40, 0xB7, 0x98, 0xBA, 0x4E, 0x15, 0x8C,
0xB3, 0x00, 0x1C, 0x93, 0x3E, 0x1D, 0x69, 0x03, 0x26, 0x03, 0x75, 0x35, 0x46, 0x5A, 0x81, 0xC1,
0xCC, 0x03, 0xC3, 0x2B, 0xFB, 0xF3, 0x1E, 0x16, 0xBF, 0xFB, 0x97, 0xAA, 0xAA, 0x81, 0xD4, 0x8B,
0x33, 0x5D, 0x59, 0x59, 0xD5, 0x4B, 0xE0, 0xD2, 0x08, 0xA0, 0x5B, 0x8B, 0x3C, 0x3A, 0x8C, 0xFC,
0x87, 0x52, 0xF6, 0x4D, 0xBB, 0x0F, 0x87, 0x01, 0x49, 0xD3, 0x73, 0xB8, 0x01, 0x43, 0xF7, 0x42,
0x50, 0xB8, 0xB2, 0xC2, 0xFD, 0xE6, 0xE6, 0x66, 0x15, 0x29, 0xA1, 0x21, 0x14, 0xDB, 0x8A, 0x2B,
0xF0, 0x49, 0xD3, 0xF1, 0x81, 0x30, 0x18, 0xD2, 0x1A, 0xC6, 0xF0, 0x25, 0xE3, 0x47, 0x5C, 0x71,
0xF4, 0xF4, 0x22, 0xA6, 0xFC, 0x33, 0xDC, 0x95, 0x32, 0xCB, 0x1A, 0xAD, 0xA6, 0x68, 0xFA, 0x8F,
0xD8, 0x3E, 0xCA, 0x0D, 0x76, 0xC1, 0x7A, 0xBA, 0x56, 0xA1, 0xFC, 0x9F, 0x61, 0xB9, 0x94, 0x28,
0xD6, 0x70, 0x9C, 0x40, 0x80, 0x5A, 0xC3, 0x31, 0xC4, 0x1A, 0x41, 0x17, 0xFC, 0x26, 0x6B, 0xF9,
0xCD, 0xFE, 0x19, 0x7E, 0x97, 0x76, 0x1E, 0x15, 0x25, 0x91, 0xAA, 0xAF, 0x50, 0x02, 0x9F, 0xDD,
0xE9, 0xA6, 0x15, 0xB9, 0x55, 0x0A, 0x50, 0x1B, 0x46, 0x41, 0xD0, 0x8F, 0xE2, 0x83, 0x27, 0xD6,
0x9D, 0xC5, 0x7A, 0x31, 0xC8, 0xD9, 0x5C, 0x6E, 0xB1, 0xBC, 0xB5, 0x44, 0x4F, 0xA1, 0xEC, 0x5F,
0x4B, 0x15, 0x01, 0x3F, 0x23, 0x8B, 0x7B, 0xAC, 0xD4, 0xA5, 0x36, 0x28, 0x0F, 0x56, 0x3F, 0xD5,
0x3C, 0xCB, 0x5F, 0xCC, 0xAE, 0x6B, 0x51, 0x9B, 0xC0, 0x38, 0x57, 0x92, 0x8B, 0x4A, 0xB2, 0xC8,
0x13, 0x01, 0xA8, 0x58, 0xC7, 0x2E, 0xC4, 0x4D, 0x6B, 0x7A, 0x7C, 0xBF, 0x5C, 0x83, 0xC2, 0xDF,
0xF5, 0xD5, 0x12, 0x33, 0x08, 0xC4, 0xD3, 0x95, 0x4B, 0x29, 0x5F, 0x37, 0x29, 0x8A, 0x0E, 0x62,
0x47, 0xA3, 0x51, 0x4A, 0xC5, 0x47, 0x0C, 0x49, 0x56, 0xB2, 0x98, 0x9F, 0xC8, 0x90, 0x04, 0x8C,
0x45, 0x3C, 0x8C, 0xB2, 0x94, 0x46, 0x99, 0xA8, 0xA4, 0x16, 0x63, 0x21, 0xCC, 0x5E, 0xFA, 0xE7,
0x9F, 0x8B, 0xC9, 0x7E, 0x5A, 0x0B, 0x96, 0xD3, 0xEB, 0x3D, 0xBF, 0x34, 0xD9, 0xF7, 0x6B, 0x89,
0xB9, 0x7A, 0xE9, 0xFF, 0x67, 0x4B, 0x21, 0x65, 0x4B, 0xF1, 0xB0, 0x54, 0x2E, 0x62, 0x62, 0x29,
0xE6, 0xC9, 0x82, 0x91, 0x97, 0x7C, 0x16, 0x0D, 0x1A, 0x2B, 0x25, 0x55, 0x9E, 0x97, 0x7D, 0x95,
0x43, 0x40, 0x59, 0x71, 0xE5, 0x35, 0x11, 0x06, 0x34, 0xE0, 0x63, 0x64, 0xF2, 0x41, 0xEB, 0xA7,
0xD1, 0x94, 0x26, 0x87, 0x24, 0xA5, 0x06, 0x24, 0xCD, 0x65, 0xDC, 0x41, 0xA8, 0xE9, 0x04, 0xEB,
0x76, 0x6D, 0x6E, 0x12, 0x05, 0xCE, 0x33, 0x77, 0xC4, 0xB1, 0x26, 0x03, 0xF9, 0xB2, 0xCA, 0x09,
0xD4, 0xC6, 0xBE, 0x12, 0xA4, 0x3E, 0x52, 0x25, 0xA8, 0x61, 0x5A, 0xD0, 0x76, 0xC0, 0x35, 0x5F,
0x26, 0x51, 0x4C, 0xC6, 0xB2, 0x07, 0x83, 0x35, 0x74, 0x0F, 0xA4, 0x66, 0x6D, 0x34, 0x91, 0x60,
0xA9, 0x73, 0x29, 0xFC, 0x66, 0xD9, 0xC2, 0x70, 0x4B, 0x57, 0xC9, 0xB0, 0xBD, 0xF4, 0xA5, 0x35,
0x59, 0x83, 0xE0, 0x0B, 0x6C, 0x62, 0xE0, 0x1E, 0x68, 0x64, 0xF2, 0x7B, 0x00, 0x77, 0x6B, 0xB6,
0xA3, 0x3D, 0xD6, 0x8E, 0x6A, 0x35, 0x53, 0x55, 0xE9, 0xAE, 0x0B, 0x6D, 0x4E, 0x74, 0x23, 0x0B,
0x4B, 0x10, 0xAA, 0x9A, 0x59, 0x0C, 0x38, 0x1B, 0x81, 0xAA, 0xBA, 0xC0, 0x11, 0xD6, 0x98, 0x66,
0xA9, 0x23, 0xF1, 0x97, 0x1D, 0xC9, 0x13, 0xB5, 0x07, 0x95, 0xF5, 0x05, 0xD4, 0x31, 0xAB, 0x25,
0x86, 0x30, 0xD3, 0x29, 0x13, 0xDE, 0x04, 0x03, 0x90, 0x07, 0x5A, 0xD5, 0x05, 0x14, 0xB5, 0x8E,
0x1C, 0x4D, 0x44, 0xB8, 0x1C, 0x05, 0xF9, 0xF0, 0x6B, 0x9A, 0x0F, 0xBC, 0xB4, 0x18, 0xDD, 0x97,
0x80, 0x50, 0xD2, 0xE6, 0xE0, 0x88, 0x8F, 0xF2, 0x21, 0xF4, 0xB2, 0x05, 0x9D, 0x02, 0x58, 0xFC,
0xC6, 0x71, 0x3E, 0x8A, 0x27, 0xC5, 0x68, 0x42, 0xEF, 0x17, 0x78, 0x51, 0x01, 0xF5, 0xA9, 0xEE,
0x28, 0x1B, 0xDB, 0x68, 0xCE, 0xF3, 0x41, 0x6B, 0x29, 0x7F, 0xF0, 0xFF, 0x28, 0x7F, 0xCC, 0xC7,
0x85, 0x34, 0x71, 0x31, 0x1A, 0xB3, 0x42, 0x96, 0x61, 0x18, 0xFF, 0x90, 0x93, 0xA4, 0xD4, 0x13,
0x97, 0x7A, 0x5A, 0xF1, 0xB3, 0xB6, 0x53, 0x98, 0x8E, 0x31, 0xAA, 0xF8, 0xE3, 0xC8, 0xF6, 0xF0,
0xF7, 0x3C, 0xF2, 0x65, 0x6D, 0x69, 0x5A, 0xA1, 0x31, 0x82, 0x3A, 0x57, 0x37, 0xCB, 0x7E, 0x9A,
0xFD, 0xB7, 0xAD, 0xE8, 0xD1, 0xF1, 0xE9, 0x71, 0xFF, 0xB8, 0x5C, 0x38, 0x23, 0xE7, 0x25, 0x93,
0x8A, 0x2B, 0x5D, 0xFA, 0xB2, 0x22, 0x80, 0x02, 0x1B, 0x45, 0x01, 0x7B, 0xDD, 0xDC, 0x54, 0x7E,
0xF1, 0xB6, 0x77, 0x71, 0x6E, 0xC7, 0x24, 0x01, 0x8F, 0x24, 0x15, 0xE6, 0xC2, 0x82, 0x44, 0xF9,
0xE0, 0xD7, 0xC7, 0xA5, 0x72, 0x5D, 0x7E, 0x61, 0x70, 0xC4, 0xDC, 0x52, 0xA7, 0xA9, 0x7E, 0x78,
0xE2, 0x62, 0x5D, 0x99, 0xBF, 0x04, 0x41, 0x72, 0x1A, 0x2D, 0x13, 0x55, 0x11, 0x67, 0x46, 0xE5,
0x30, 0x2F, 0xEE, 0xB2, 0x75, 0x0D, 0xD3, 0xC8, 0xB4, 0xC4, 0x84, 0xA5, 0xE5, 0x46, 0xA5, 0x12,
0x14, 0xFE, 0xA2, 0xB6, 0xE7, 0x8B, 0x91, 0x24, 0xB7, 0x5A, 0x73, 0xAB, 0x6F, 0x41, 0x2A, 0x3E,
0x58, 0x04, 0x23, 0x66, 0x39, 0xDB, 0x16, 0x77, 0xA3, 0x43, 0xEE, 0x61, 0x5C, 0x7F, 0xBA, 0x35,
0x78, 0xD2, 0x3C, 0x79, 0x61, 0x9E, 0xFC, 0xB1, 0x7B, 0x2E, 0x1C, 0x45, 0xF9, 0xDA, 0xE2, 0x98,
0xF6, 0x10, 0x58, 0xBB, 0x6D, 0x2F, 0x7D, 0x18, 0x20, 0xD2, 0x83, 0xCB, 0x00, 0xF4, 0x63, 0x58,
0xFF, 0x4A, 0xEE, 0x88, 0x7A, 0x09, 0xAA, 0xA2, 0xAD, 0x73, 0x54, 0xD8, 0xEE, 0xFD, 0x81, 0xA3,
0xF2, 0xCE, 0x65, 0x18, 0x48, 0x97, 0xC3, 0x92, 0x37, 0x8B, 0x75, 0xC1, 0x61, 0x19, 0x31, 0x64,
0x6C, 0x00, 0xE3, 0xCD, 0x5D, 0x49, 0x13, 0xD5, 0x1C, 0xB4, 0xF0, 0x1B, 0x08, 0x8A, 0x4F, 0x39,
0xCE, 0x9A, 0x38, 0xAD, 0x62, 0x72, 0xC5, 0x23, 0xC8, 0x4A, 0x67, 0x89, 0xC0, 0x6E, 0x10, 0x0D,
0x0D, 0x7C, 0x64, 0x9A, 0xA1, 0xB6, 0x1D, 0x3E, 0x37, 0xD7, 0xBC, 0xD9, 0x54, 0xFA, 0x4B, 0x62,
0x79, 0xD5, 0xB0, 0x8B, 0x1C, 0x56, 0xCC, 0x75, 0x7D, 0x1F, 0xF4, 0xA3, 0x4E, 0x29, 0xAF, 0x48,
0xA4, 0x53, 0xD1, 0x83, 0xC4, 0x86, 0xA2, 0x41, 0xBE, 0x91, 0x40, 0x44, 0x72, 0x4A, 0x33, 0x5D,
0xC7, 0xCA, 0xD2, 0x0B, 0x28, 0x49, 0x7A, 0xB2, 0x73, 0x95, 0x49, 0x6B, 0x25, 0x06, 0xFE, 0xC8,
0xD7, 0xF0, 0xC7, 0xA1, 0xD0, 0xA3, 0x83, 0x9B, 0x49, 0x2B, 0x83, 0xA4, 0x23, 0x64, 0x83, 0xA9,
0x37, 0xE4, 0xBB, 0xA8, 0x2D, 0x2F, 0xCB, 0xB4, 0x16, 0x50, 0x70, 0x71, 0x83, 0xBB, 0x11, 0x30,
0x52, 0x5A, 0xC4, 0x9E, 0x94, 0xA8, 0xC7, 0x8F, 0x10, 0x1F, 0x53, 0x4A, 0x20, 0x06, 0x20, 0xA6,
0x40, 0xD0, 0xA7, 0x42, 0x8A, 0x54, 0xE6, 0x92, 0x53, 0x2A, 0x20, 0xCA, 0x48, 0xCD, 0xE2, 0xC1,
0x85, 0x78, 0xD4, 0x46, 0xD6, 0x80, 0xFD, 0xDC, 0xBD, 0x73, 0x33, 0xDE, 0x90, 0x68, 0x09, 0x56,
0x36, 0x3D, 0x9A, 0xA6, 0x52, 0x5C, 0x54, 0xC7, 0x19, 0xF8, 0xA8, 0xA1, 0x03, 0x5A, 0x23, 0x84,
0x11, 0x1E, 0x84, 0x8A, 0x01, 0x40, 0x7F, 0x42, 0xC3, 0x1C, 0x22, 0x70, 0x08, 0x20, 0x82, 0xA0,
0x7F, 0x49, 0x0D, 0xF7, 0x64, 0x05, 0xC9, 0xF8, 0xD8, 0x6D, 0x35, 0xF0, 0x9D, 0x66, 0x95, 0xEC,
0x20, 0xA5, 0xBD, 0x68, 0x24, 0xFA, 0x64, 0x98, 0x1A, 0x50, 0x00, 0xAC, 0xD9, 0x01, 0xA0, 0x1E,
0x24, 0x5E, 0x63, 0x2B, 0x3F, 0xEF, 0x04, 0x2A, 0xBB, 0x00, 0xAB, 0xBB, 0x8E, 0x87, 0x5F, 0x39,
0x4F, 0x19, 0xA7, 0x39, 0x26, 0x00, 0x7B, 0x93, 0x68, 0x7A, 0x99, 0x30, 0x2E, 0xCE, 0x64, 0x1B,
0x6A, 0x6C, 0xB4, 0xE4, 0xF5, 0xA9, 0x87, 0x15, 0x79, 0x3F, 0xC5, 0x8B, 0xCB, 0x0C, 0xF3, 0xBA,
0x53, 0x79, 0x77, 0xB1, 0x86, 0x70, 0x21, 0x50, 0x66, 0x38, 0xB3, 0x29, 0x74, 0xB0, 0xFA, 0xA1,
0x48, 0x82, 0x7A, 0x4F, 0xB7, 0x42, 0xE2, 0xC1, 0x44, 0xED, 0x81, 0xF9, 0xDC, 0xC2, 0xD8, 0xE1,
0x94, 0x83, 0x5A, 0x0A, 0xB5, 0x02, 0x45, 0xC6, 0x95, 0xCD, 0x98, 0x35, 0x1D, 0x6A, 0x58, 0x88,
0x61, 0xE0, 0xAF, 0xFE, 0x05, 0x0F, 0x1E, 0x1C, 0xC8, 0x55, 0x3F, 0xE1, 0x23, 0xE3, 0x7E, 0xF4,
0x23, 0x3E, 0x3E, 0xAF, 0xF0, 0xF1, 0x79, 0x1D, 0x1F, 0xB4, 0xAA, 0x3C, 0x98, 0x0C, 0x80, 0xEC,
0x19, 0xE1, 0x64, 0x4C, 0x13, 0x58, 0xC0, 0x43, 0x50, 0x25, 0x7F, 0x8B, 0xB3, 0x84, 0xFE, 0x98,
0xB3, 0xDE, 0x84, 0x8D, 0xC4, 0x23, 0xFE, 0x8A, 0xD5, 0xFF, 0x82, 0x4B, 0x3C, 0x70, 0x3D, 0x97,
0x79, 0x6D, 0x5A, 0x49, 0x28, 0x3F, 0x7E, 0x2B, 0x91, 0x7E, 0xE4, 0x42, 0x78, 0xA9, 0x38, 0xC8,
0xDF, 0xB7, 0xF4, 0x00, 0xBC, 0x11, 0xF8, 0x29, 0x35, 0x75, 0xBC, 0x0B, 0xA5, 0xFC, 0x29, 0x30,
0x64, 0xA8, 0xC0, 0x47, 0xDD, 0xD9, 0xDC, 0x12, 0xAE, 0x01, 0x8A, 0xF1, 0xA3, 0x29, 0xB0, 0xEA,
0xC9, 0x02, 0xD7, 0x9E, 0x40, 0x26, 0x04, 0x91, 0xE0, 0x48, 0xC8, 0xA7, 0x8D, 0x2F, 0x07, 0x9B,
0x37, 0x35, 0xC8, 0x43, 0x2E, 0xFC, 0x98, 0x2E, 0x0C, 0x36, 0x6F, 0xFE, 0x6D, 0x36, 0xC6, 0xCC,
0x5A, 0x76, 0xA4, 0x96, 0x4C, 0xF6, 0xF4, 0x0B, 0xBF, 0x71, 0x09, 0x48, 0x5D, 0x49, 0x78, 0x45,
0x34, 0x03, 0x6B, 0x43, 0x61, 0xE1, 0x07, 0xFF, 0x47, 0x09, 0xF8, 0x91, 0x9E, 0x07, 0xCE, 0xBD,
0xE6, 0x3D, 0x5E, 0x2F, 0x3E, 0x85, 0xE9, 0x56, 0xE9, 0xC1, 0x4A, 0xC7, 0xEF, 0x53, 0x3A, 0x76,
0x59, 0xA2, 0x14, 0x4A, 0x14, 0x59, 0x88, 0x1A, 0x6A, 0x50, 0x0E, 0x51, 0x98, 0x89, 0x17, 0xCD,
0x81, 0x02, 0x9B, 0x73, 0x34, 0x5B, 0x3A, 0x02, 0x0F, 0xF4, 0xF5, 0x45, 0xEE, 0xFC, 0x74, 0x76,
0x7A, 0x22, 0x44, 0x7C, 0xA5, 0x62, 0x22, 0xD0, 0xAA, 0x2E, 0x2C, 0x2F, 0xCF, 0x9C, 0x89, 0xE4,
0xA1, 0x28, 0x75, 0x30, 0x31, 0x28, 0x87, 0xFE, 0x74, 0x31, 0xFC, 0x0A, 0x71, 0xD6, 0xD0, 0xCF,
0x52, 0x48, 0x58, 0x5B, 0x36, 0xA2, 0xF7, 0xFB, 0x97, 0xF6, 0xAE, 0xDD, 0x84, 0xBA, 0x00, 0xB4,
0x0A, 0x69, 0x19, 0xEE, 0x7D, 0xFE, 0xB7, 0x90, 0xB7, 0xFF, 0x1E, 0x32, 0x83, 0xA8, 0x95, 0x42,
0x58, 0x2A, 0xF0, 0xAB, 0xB8, 0x93, 0x24, 0x9A, 0x4A, 0xB4, 0xE3, 0x24, 0xC1, 0x4B, 0xE9, 0x43,
0x85, 0xA2, 0x0D, 0x61, 0x31, 0xA5, 0x89, 0xE6, 0x47, 0x34, 0xD5, 0x78, 0x24, 0xB4, 0x34, 0x8B,
0x63, 0x68, 0x5C, 0x56, 0xF4, 0x61, 0xEB, 0xC5, 0xEB, 0xCB, 0xFB, 0x8C, 0x66, 0xD4, 0xCF, 0x97,
0x69, 0x52, 0xD1, 0x0B, 0x56, 0x50, 0xDF, 0x10, 0xEE, 0x7E, 0xB9, 0xC9, 0xEB, 0xA9, 0x8C, 0x73,
0x8C, 0xA2, 0x1B, 0x2D, 0x35, 0x07, 0xE9, 0x26, 0x40, 0xD5, 0xE5, 0x59, 0x10, 0xCC, 0xDB, 0x2B,
0xB4, 0xA0, 0xF1, 0x8A, 0x44, 0x24, 0x9F, 0xCB, 0x67, 0x7F, 0xE4, 0xC9, 0xA9, 0xE2, 0x82, 0x50,
0xF2, 0x54, 0xA9, 0x36, 0xAD, 0x0D, 0x63, 0x83, 0x6A, 0x8C, 0xA7, 0x82, 0x70, 0x0F, 0xAF, 0x51,
0xE9, 0xC2, 0x2C, 0x6A, 0x29, 0xDC, 0xDE, 0x46, 0x5F, 0xCB, 0x6D, 0xE9, 0x89, 0x7C, 0x2A, 0x25,
0xE3, 0xAE, 0xAE, 0x63, 0x55, 0x45, 0xB1, 0x3E, 0x25, 0x61, 0x5A, 0x26, 0x5B, 0x54, 0x06, 0x26,
0x77, 0x0B, 0x70, 0x9B, 0x06, 0x29, 0x1C, 0xBD, 0x7E, 0x7F, 0xCE, 0x46, 0xD1, 0xCE, 0x11, 0x80,
0x69, 0xC5, 0x3E, 0x93, 0xD7, 0xE0, 0x24, 0xCC, 0x73, 0x07, 0x32, 0xE9, 0x4A, 0x03, 0x0E, 0xA9,
0x98, 0x44, 0xFE, 0x81, 0x7E, 0xA0, 0x3B, 0x3A, 0xFC, 0xBB, 0x09, 0x35, 0x47, 0xCD, 0xA5, 0xD0,
0xA4, 0xFA, 0x74, 0x70, 0xF5, 0x06, 0xC2, 0x53, 0x0C, 0xA5, 0x01, 0x17, 0x50, 0x34, 0xD7, 0x74,
0x7C, 0x7A, 0x7D, 0x0C, 0x29, 0xC8, 0x7F, 0x21, 0x37, 0x66, 0xBB, 0xAA, 0x6C, 0xB8, 0xF3, 0xEA,
0x75, 0x56, 0x2E, 0x03, 0x7A, 0x61, 0x8C, 0x58, 0x0F, 0x29, 0x7E, 0xFB, 0x7B, 0xF4, 0x9E, 0x8D,
0x15, 0xD2, 0x6A, 0x5D, 0x6F, 0xCE, 0x76, 0x90, 0x67, 0x89, 0xD5, 0x43, 0x2C, 0x70, 0x97, 0x1F,
0x29, 0x59, 0x95, 0x35, 0xDC, 0xF6, 0x48, 0x10, 0xE0, 0xC7, 0x5A, 0x03, 0x1B, 0x6A, 0x22, 0xB2,
0xD4, 0x42, 0x22, 0x29, 0x08, 0x90, 0xD2, 0x3E, 0x84, 0x39, 0xD3, 0x92, 0x65, 0x86, 0xB2, 0xA1,
0xBC, 0xFF, 0xC5, 0x9A, 0xA3, 0x64, 0x46, 0xE8, 0xCE, 0xF9, 0x6C, 0x73, 0x53, 0xD8, 0x85, 0x99,
0x18, 0x05, 0x52, 0x8A, 0x01, 0x1C, 0x9A, 0x7D, 0x68, 0x2D, 0x8C, 0xB2, 0x90, 0x58, 0xAB, 0x3D,
0xD2, 0xB6, 0x51, 0x55, 0x03, 0x54, 0x7C, 0x46, 0x01, 0x03, 0xCE, 0xB2, 0x24, 0x80, 0xA8, 0x8B,
0x39, 0xBA, 0xB2, 0x2D, 0xC5, 0xBA, 0xD0, 0x84, 0x0E, 0xEC, 0x67, 0xC8, 0x12, 0x95, 0x97, 0xAD,
0xA2, 0x27, 0x12, 0xC5, 0x77, 0x95, 0x9E, 0xC8, 0x6F, 0xE5, 0x84, 0xAA, 0xC8, 0x77, 0x88, 0x2F,
0x13, 0x5C, 0xD4, 0xD1, 0x13, 0xA0, 0x24, 0x83, 0x52, 0x34, 0x60, 0x2A, 0x2C, 0x37, 0xEE, 0xEB,
0xD3, 0xE9, 0xB4, 0x8E, 0xDF, 0x6A, 0xEB, 0x70, 0x82, 0xB2, 0x02, 0x5F, 0x5F, 0xC7, 0x21, 0x47,
0x15, 0x58, 0xF8, 0x6E, 0xE1, 0xAC, 0xBA, 0xE8, 0x42, 0x7F, 0x2B, 0xDE, 0xD4, 0xAA, 0xD2, 0x59,
0xE1, 0x73, 0x79, 0xDB, 0x7B, 0x3B, 0x2B, 0x20, 0x32, 0xC4, 0xAF, 0xB2, 0x90, 0x69, 0x20, 0x0D,
0x3B, 0xE5, 0x46, 0x56, 0x25, 0x85, 0x65, 0x5C, 0xB0, 0xE3, 0x2C, 0x9D, 0x18, 0x33, 0x60, 0xDD,
0x11, 0x96, 0xD2, 0x95, 0x43, 0x2D, 0x65, 0xB7, 0x0E, 0xB7, 0x0A, 0xFB, 0x70, 0x30, 0x83, 0x94,
0x79, 0xFB, 0xF3, 0x4F, 0x39, 0x5B, 0xDE, 0xF6, 0x92, 0x62, 0x71, 0xE1, 0xF3, 0xFC, 0xA9, 0x35,
0xAF, 0x69, 0xA5, 0xD1, 0xAF, 0xC4, 0x97, 0xBD, 0x46, 0xFE, 0x19, 0x3B, 0xFF, 0x9C, 0xAD, 0x81,
0xB1, 0x43, 0x23, 0x2A, 0xDC, 0x4C, 0x8C, 0xEA, 0x2F, 0x34, 0xE6, 0x63, 0x79, 0x29, 0xBF, 0x2D,
0xA0, 0x54, 0xA9, 0xD3, 0x68, 0x78, 0x3E, 0xFF, 0x9A, 0x42, 0x19, 0x1D, 0x65, 0xFE, 0x28, 0x20,
0x09, 0xC5, 0x82, 0xA3, 0x41, 0xBE, 0x92, 0xFB, 0x46, 0xC0, 0x86, 0x69, 0x03, 0x93, 0x6D, 0xCB,
0xDE, 0xB2, 0x77, 0x71, 0x64, 0x7F, 0x4D, 0xF7, 0x57, 0x4F, 0xD8, 0x5F, 0x34, 0x69, 0x58, 0x0B,
0xE7, 0xB5, 0xAB, 0x8A, 0x4D, 0x6A, 0x83, 0xFB, 0xC4, 0xA7, 0x70, 0x3D, 0x6F, 0xB3, 0xCC, 0xB6,
0x1A, 0xE4, 0x5F, 0x60, 0xD4, 0x31, 0xBA, 0x95, 0x2F, 0x92, 0xF4, 0x81, 0x7B, 0x18, 0x5B, 0x17,
0x54, 0x26, 0x70, 0x49, 0xD5, 0x87, 0x34, 0xB9, 0xD3, 0x9C, 0x2F, 0x39, 0xC3, 0xB7, 0x3C, 0xA8,
0x03, 0xE4, 0x37, 0x9C, 0x72, 0x39, 0xB0, 0xBF, 0x07, 0x5D, 0x33, 0x2A, 0x41, 0x79, 0xB1, 0x26,
0x9B, 0xE6, 0x7C, 0x02, 0x82, 0x01, 0x70, 0xB1, 0xA3, 0x48, 0xCD, 0x2B, 0xCB, 0x98, 0x9B, 0x57,
0x96, 0x54, 0xE2, 0x5F, 0x59, 0xCC, 0xDB, 0x9F, 0xFC, 0xDB, 0x4C, 0xF9, 0x7F, 0x5B, 0x28, 0x36,
0x32, 0xF9, 0xE1, 0x09, 0xF7, 0x56, 0x3F, 0x45, 0xAD, 0x47, 0x51, 0xBB, 0xF7, 0xFF, 0x17, 0x53,
0xE8, 0x9D, 0x36, 0x92, 0x29, 0x00, 0x00};
#define SPIFFS_MAXLENGTH_FILEPATH 32
const char *excludeListFile = "/.exclude.files";
typedef struct ExcludeListS {
char *item;
ExcludeListS *next;
} ExcludeList;
static ExcludeList *excludes = NULL;
static bool matchWild(const char *pattern, const char *testee) {
const char *nxPat = NULL;
const char *nxTst = NULL;
while (*testee) {
if ((*pattern == '?') || (*pattern == *testee)) {
++pattern;
++testee;
continue;
}
if (*pattern == '*') {
nxPat = pattern++;
nxTst = testee;
continue;
}
if (nxPat) {
pattern = nxPat + 1;
testee = ++nxTst;
continue;
}
return false;
}
while (*pattern == '*') {
++pattern;
}
return (*pattern == 0);
}
static bool addExclude(const char *item) {
size_t len = strlen(item);
if (!len) {
return false;
}
ExcludeList *e = (ExcludeList *)malloc(sizeof(ExcludeList));
if (!e) {
return false;
}
e->item = (char *)malloc(len + 1);
if (!e->item) {
free(e);
return false;
}
memcpy(e->item, item, len + 1);
e->next = excludes;
excludes = e;
return true;
}
static void loadExcludeList(fs::FS &_fs, const char *filename) {
static char linebuf[SPIFFS_MAXLENGTH_FILEPATH];
fs::File excludeFile;
if (filename[0] != '/') {
excludeFile = _fs.open("/" + ((String)filename), "r");
} else {
excludeFile = _fs.open(filename, "r");
}
if (!excludeFile) {
// addExclude("/*.js.gz");
return;
}
#ifdef ESP32
if (excludeFile.isDirectory()) {
excludeFile.close();
return;
}
#endif
if (excludeFile.size() > 0) {
uint8_t idx;
bool isOverflowed = false;
while (excludeFile.available()) {
linebuf[0] = '\0';
idx = 0;
int lastChar;
do {
lastChar = excludeFile.read();
if (lastChar != '\r') {
linebuf[idx++] = (char)lastChar;
}
} while ((lastChar >= 0) && (lastChar != '\n') && (idx < SPIFFS_MAXLENGTH_FILEPATH));
if (isOverflowed) {
isOverflowed = (lastChar != '\n');
continue;
}
isOverflowed = (idx >= SPIFFS_MAXLENGTH_FILEPATH);
linebuf[idx - 1] = '\0';
if (!addExclude(linebuf)) {
excludeFile.close();
return;
}
}
}
excludeFile.close();
}
static bool isExcluded(fs::FS &_fs, const char *filename) {
if (excludes == NULL) {
loadExcludeList(_fs, excludeListFile);
}
ExcludeList *e = excludes;
while (e) {
if (matchWild(e->item, filename)) {
return true;
}
e = e->next;
}
return false;
}
// WEB HANDLER IMPLEMENTATION
#ifdef ESP32
SPIFFSEditor::SPIFFSEditor(const fs::FS &fs, const String &username, const String &password)
#else
SPIFFSEditor::SPIFFSEditor(const String &username, const String &password, const fs::FS &fs)
#endif
: _fs(fs), _username(username), _password(password), _authenticated(false), _startTime(0) {
}
@@ -406,24 +19,20 @@ bool SPIFFSEditor::canHandle(AsyncWebServerRequest *request) {
if (!request->_tempFile) {
return false;
}
#ifdef ESP32
if (request->_tempFile.isDirectory()) {
request->_tempFile.close();
return false;
}
#endif
}
if (request->hasParam("download")) {
request->_tempFile = _fs.open("/" + request->arg("download"), "r");
if (!request->_tempFile) {
return false;
}
#ifdef ESP32
if (request->_tempFile.isDirectory()) {
request->_tempFile.close();
return false;
}
#endif
}
request->addInterestingHeader("If-Modified-Since");
return true;
@@ -442,46 +51,22 @@ void SPIFFSEditor::handleRequest(AsyncWebServerRequest *request) {
if (request->method() == HTTP_GET) {
if (request->hasParam("list")) {
const String path = request->getParam("list")->value();
#ifdef ESP32
File dir = _fs.open(path);
#else
Dir dir = _fs.openDir(path);
#endif
String output = "[";
#ifdef ESP32
File file = dir.openNextFile();
while (file) {
#else
while (dir.next()) {
fs::File entry = dir.openFile("r");
#endif
if (isExcluded(_fs, file.name())) {
#ifdef ESP32
file = dir.openNextFile();
#endif
continue;
}
if (output != "[") {
output += ',';
}
output += "{\"type\":\"";
output += "file";
output += "\",\"name\":\"";
output += file.name();
output += "\",\"size\":";
output += file.size();
output += ",\"ver\":";
output += file.getLastWrite();
output += "}";
#ifdef ESP32
if (file.isDirectory()) {
output += "{\"type\":\"dir\",\"name\":\"" + String(file.name()) + "\",\"size\":" + file.size() + "}";
} else {
output += "{\"type\":\"file\",\"name\":\"" + String(file.name()) + "\",\"size\":" + file.size() + "}";
}
file = dir.openNextFile();
#else
file.close();
#endif
}
#ifdef ESP32
dir.close();
#endif
output += "]";
request->send(200, "application/json", output);
} else if (request->hasParam("edit") || request->hasParam("download")) {
@@ -491,8 +76,7 @@ void SPIFFSEditor::handleRequest(AsyncWebServerRequest *request) {
if (request->header("If-Modified-Since").equals(buildTime)) {
request->send(304);
} else {
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", edit_htm_gz, edit_htm_gz_len);
response->addHeader("Content-Encoding", "gzip");
AsyncWebServerResponse *response = request->beginResponse(_fs, "/www/edit.html");
response->addHeader("Last-Modified", buildTime);
request->send(response);
}

View File

@@ -56,7 +56,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
deserializeJson(doc, taginfo->modeConfigJson);
JsonObject cfgobj = doc.as<JsonObject>();
Serial.println("Updating " + dst + " mode " + String(taginfo->contentMode) + " nextupdate " + String(taginfo->nextupdate));
wsLog("Updating " + dst + " mode " + String(taginfo->contentMode));
taginfo->nextupdate = now + 600;
switch (taginfo->contentMode) {
@@ -67,7 +67,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
if (prepareDataAvail(&filename, DATATYPE_IMGRAW, mac, cfgobj["timetolive"].as<int>())) {
cfgobj["#fetched"] = true;
} else {
wsString("Error accessing " + filename);
wsErr("Error accessing " + filename);
}
taginfo->nextupdate = 3216153600;
}
@@ -75,21 +75,17 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
case Today:
Serial.println("heap voor drawDate: " + String(ESP.getFreeHeap()));
drawDate(filename);
Serial.println("heap na drawDate: " + String(ESP.getFreeHeap()));
drawDate(filename, taginfo);
// updateTagImage(filename, mac, (midnight - now) / 60 - 10);
updateTagImage(filename, mac, 60);
updateTagImage(filename, mac, 0);
taginfo->nextupdate = midnight;
break;
case CountDays:
if (buttonPressed) cfgobj["counter"] = 0;
Serial.println("heap voor drawnumber: " + String(ESP.getFreeHeap()));
drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"]);
Serial.println("heap na drawnumber: " + String(ESP.getFreeHeap()));
updateTagImage(filename, mac, 60);
drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"], taginfo);
updateTagImage(filename, mac, (buttonPressed?0:60));
cfgobj["counter"] = (int32_t)cfgobj["counter"] + 1;
taginfo->nextupdate = midnight;
break;
@@ -97,12 +93,10 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
case CountHours:
if (buttonPressed) cfgobj["counter"] = 0;
Serial.println("heap voor drawnumber: " + String(ESP.getFreeHeap()));
drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"]);
Serial.println("heap na drawnumber: " + String(ESP.getFreeHeap()));
drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"], taginfo);
// updateTagImage(&filename, mac, (3600 - now % 3600) / 60);
// taginfo->nextupdate = now + 3600 - (now % 3600);
updateTagImage(filename, mac, 3);
updateTagImage(filename, mac, (buttonPressed?0:3));
cfgobj["counter"] = (int32_t)cfgobj["counter"] + 1;
taginfo->nextupdate = now + 300;
break;
@@ -110,6 +104,13 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
case Weather:
// https://open-meteo.com/
// https://geocoding-api.open-meteo.com/v1/search?name=eindhoven
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true
// https://github.com/erikflowers/weather-icons
drawWeather(filename, cfgobj["location"], taginfo);
updateTagImage(filename, mac, 0);
taginfo->nextupdate = now + 900;
break;
case Firmware:
@@ -119,7 +120,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
if (prepareDataAvail(&filename, DATATYPE_UPDATE, mac, cfgobj["timetolive"].as<int>())) {
cfgobj["#fetched"] = true;
} else {
wsString("Error accessing " + filename);
wsErr("Error accessing " + filename);
}
cfgobj["filename"]="";
taginfo->nextupdate = 3216153600;
@@ -149,8 +150,7 @@ bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin) {
return true;
}
void drawDate(String &filename) {
void drawDate(String &filename, tagRecord *&taginfo) {
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);
time_t now;
@@ -161,59 +161,291 @@ void drawDate(String &filename) {
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;
long w,h;
LittleFS.begin();
long w = 296, h = 128; // mag staand of liggend
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
Serial.println("Failed to create sprite in drawDate");
if (taginfo->model == SOLUM_29_033) {
w = 296;
h = 128;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite in drawDate");
}
spr.setColorDepth(8);
spr.fillSprite(TFT_WHITE);
spr.setTextDatum(TC_DATUM);
spr.loadFont("fonts/calibrib62", LittleFS);
spr.setTextColor(TFT_RED, TFT_WHITE);
spr.drawString(Dag[timeinfo.tm_wday], w / 2, 10);
spr.loadFont("fonts/calibrib50", LittleFS);
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.drawString(String(timeinfo.tm_mday) + " " + Maand[timeinfo.tm_mon], w / 2, 73);
spr.unloadFont();
} else if (taginfo->model == SOLUM_154_033) {
w = 154;
h = 154;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite in drawDate");
}
spr.setColorDepth(8);
spr.fillSprite(TFT_WHITE);
spr.setTextDatum(TC_DATUM);
spr.loadFont("fonts/calibrib30", LittleFS);
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.drawString(Dag[timeinfo.tm_wday], w / 2, 10);
spr.drawString(String(Maand[timeinfo.tm_mon]), w / 2, 120);
spr.unloadFont();
spr.loadFont("fonts/numbers2-1", LittleFS);
spr.setTextColor(TFT_RED, TFT_WHITE);
spr.drawString(String(timeinfo.tm_mday), w / 2, 42);
spr.unloadFont();
}
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) {
void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord *&taginfo) {
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);
LittleFS.begin();
long w = 296, h = 128;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
Serial.println("Failed to create sprite in drawNumber");
}
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);
long w, h;
if (taginfo->model == SOLUM_29_033) {
w = 296;
h = 128;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite in drawNumber");
}
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 = "fonts/numbers1-2";
if (count > 999) font = "fonts/numbers2-2";
if (count > 9999) font = "fonts/numbers3-2";
spr.loadFont(font, LittleFS);
spr.drawString(String(count), w / 2, h / 2 + 10);
spr.unloadFont();
} else if (taginfo->model == SOLUM_154_033) {
w = 154;
h = 154;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite in drawNumber");
}
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 = "fonts/numbers1-1";
if (count > 99) font = "fonts/numbers2-1";
if (count > 999) font = "fonts/numbers3-1";
spr.loadFont(font, LittleFS);
spr.drawString(String(count), w / 2, h / 2 + 10);
spr.unloadFont();
}
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();
}
void drawWeather(String &filename, String location, tagRecord *&taginfo) {
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);
wsLog("get weather");
HTTPClient http;
http.begin("https://geocoding-api.open-meteo.com/v1/search?name=" + urlEncode(location.c_str()) + "&count=1");
http.setTimeout(5000); // timeout in ms
int httpCode = http.GET();
if (httpCode == 200) {
StaticJsonDocument<1000> doc;
DeserializationError error = deserializeJson(doc, http.getStream());
http.end();
http.begin("https://api.open-meteo.com/v1/forecast?latitude=" + doc["results"][0]["latitude"].as<String>() + "&longitude=" + doc["results"][0]["longitude"].as<String>() + "&current_weather=true&windspeed_unit=ms&timezone=" + doc["results"][0]["timezone"].as<String>());
http.setTimeout(5000); // timeout in ms
int httpCode = http.GET();
if (httpCode == 200) {
StaticJsonDocument<200> filter;
filter["current_weather"]["temperature"] = true;
filter["current_weather"]["windspeed"] = true;
filter["current_weather"]["winddirection"] = true;
filter["current_weather"]["weathercode"] = true;
doc.clear();
DeserializationError error = deserializeJson(doc, http.getString(), DeserializationOption::Filter(filter));
if (error) {
Serial.println(F("deserializeJson() failed: "));
Serial.println(error.c_str());
}
auto temperature = doc["current_weather"]["temperature"].as<double>();
auto windspeed = doc["current_weather"]["windspeed"].as<int>();
auto winddirection = doc["current_weather"]["winddirection"].as<int>();
uint8_t weathercode = doc["current_weather"]["weathercode"].as<int>();
if (weathercode > 40) weathercode -= 40;
int wind = windSpeedToBeaufort(windspeed);
String weatherIcons[] = {"\uf00d", "\uf00c", "\uf002", "\uf013", "\uf013", "\uf014", "-", "-", "\uf014", "-", "-",
"\uf01a", "-", "\uf01a", "-", "\uf01a", "\uf017", "\uf017", "-", "-", "-",
"\uf019", "-", "\uf019", "-", "\uf019", "\uf015", "\uf015", "-", "-", "-",
"\uf01b", "-", "\uf01b", "-", "\uf01b", "-", "\uf076", "-", "-", "\uf01a",
"\uf01a", "\uf01a", "-", "-", "\uf064", "\uf064", "-", "-", "-", "-",
"-", "-", "-", "-", "\uf01e", "\uf01d", "-", "-", "\uf01e"};
if (1==0) { //nacht
weatherIcons[0] = "\0uf02e";
weatherIcons[1] = "\0uf083";
weatherIcons[2] = "\0uf086";
}
String windIcons[] = {"\uf0b7", "\uf0b8", "\uf0b9", "\uf0ba", "\uf0bb", "\uf0bc", "\uf0bd", "\uf0be", "\uf0bf", "\uf0c0", "\uf0c1", "\uf0c2", "\uf0c3"};
doc.clear();
LittleFS.begin();
spr.setColorDepth(8);
tft.setTextWrap(false,false);
long w, h;
if (taginfo->model == SOLUM_29_033) {
w = 296;
h = 128;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite in drawWeather");
}
spr.fillSprite(TFT_WHITE);
spr.setTextDatum(TL_DATUM);
spr.loadFont("fonts/bahnschrift30", LittleFS);
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.drawString(location, 10, 10);
spr.unloadFont();
spr.loadFont("fonts/bahnschrift70", LittleFS);
if (temperature < 0) {
spr.setTextColor(TFT_RED, TFT_WHITE);
} else {
spr.setTextColor(TFT_BLACK, TFT_WHITE);
}
char tmpOutput[5];
dtostrf(temperature, 2, 1, tmpOutput);
spr.drawString(String(tmpOutput), 5, 65);
spr.unloadFont();
spr.loadFont("fonts/weathericons78", LittleFS);
if (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 99) {
spr.setTextColor(TFT_RED, TFT_WHITE);
} else {
spr.setTextColor(TFT_BLACK, TFT_WHITE);
}
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.setCursor(185, 20);
spr.printToSprite(weatherIcons[weathercode]);
spr.unloadFont();
spr.loadFont("fonts/weathericons30", LittleFS);
if (wind > 4) {
spr.setTextColor(TFT_RED, TFT_WHITE);
} else {
spr.setTextColor(TFT_BLACK, TFT_WHITE);
}
spr.setCursor(255, 0);
spr.printToSprite(windIcons[wind]);
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.setCursor(230, -5);
spr.printToSprite(windDirectionIcon(winddirection));
if (weathercode > 10) {
spr.setTextColor(TFT_RED, TFT_WHITE);
spr.setCursor(190, 0);
spr.printToSprite("\uf084");
}
} else if (taginfo->model == SOLUM_154_033) {
w = 154;
h = 154;
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite in drawWeather");
}
spr.fillSprite(TFT_WHITE);
spr.setTextDatum(TL_DATUM);
spr.setTextFont(2);
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.drawString(location, 10, 130);
spr.loadFont("fonts/bahnschrift30", LittleFS);
if (temperature < 0) {
spr.setTextColor(TFT_RED, TFT_WHITE);
} else {
spr.setTextColor(TFT_BLACK, TFT_WHITE);
}
char tmpOutput[5];
dtostrf(temperature, 2, 1, tmpOutput);
spr.drawString(String(tmpOutput), 10, 10);
spr.unloadFont();
spr.loadFont("fonts/weathericons78", LittleFS);
if (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 99) {
spr.setTextColor(TFT_RED, TFT_WHITE);
} else {
spr.setTextColor(TFT_BLACK, TFT_WHITE);
}
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.setCursor(35, 25);
spr.printToSprite(weatherIcons[weathercode]);
spr.unloadFont();
spr.loadFont("fonts/weathericons30", LittleFS);
if (wind > 4) {
spr.setTextColor(TFT_RED, TFT_WHITE);
} else {
spr.setTextColor(TFT_BLACK, TFT_WHITE);
}
spr.setCursor(115, -5);
spr.printToSprite(windIcons[wind]);
spr.setTextColor(TFT_BLACK, TFT_WHITE);
spr.setCursor(90, -5);
spr.printToSprite(windDirectionIcon(winddirection));
if (weathercode > 10) {
spr.setTextColor(TFT_RED, TFT_WHITE);
spr.setCursor(115, 110);
spr.printToSprite("\uf084");
}
}
spr.unloadFont();
spr2grays(spr, w, h, filename);
spr.deleteSprite();
}
}
http.end();
}
bool getImgURL(String &filename, String URL, time_t fetched) {
// https://images.klari.net/kat-bw29.jpg
@@ -233,7 +465,11 @@ bool getImgURL(String &filename, String URL, time_t fetched) {
jpg2grays(filename, filename);
}
} else {
Serial.println("http " + String(httpCode));
if (httpCode!=304) {
wsErr("http " + URL + " " + String(httpCode));
} else {
wsLog("http " + URL + " " + String(httpCode));
}
}
http.end();
return (httpCode == 200);
@@ -246,3 +482,41 @@ char *formatHttpDate(time_t t) {
strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", timeinfo);
return buf;
}
String urlEncode(const char *msg) {
const char *hex = "0123456789ABCDEF";
String encodedMsg = "";
while (*msg != '\0') {
if (
('a' <= *msg && *msg <= 'z') || ('A' <= *msg && *msg <= 'Z') || ('0' <= *msg && *msg <= '9') || *msg == '-' || *msg == '_' || *msg == '.' || *msg == '~') {
encodedMsg += *msg;
} else {
encodedMsg += '%';
encodedMsg += hex[(unsigned char)*msg >> 4];
encodedMsg += hex[*msg & 0xf];
}
msg++;
}
return encodedMsg;
}
int windSpeedToBeaufort(float windSpeed) {
int beaufort = 0;
float speeds[] = {0.3, 1.5, 3.3, 5.5, 8, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7};
for (int i = 0; i < 12; i++) {
if (windSpeed >= speeds[i]) {
beaufort = i + 1;
}
}
return beaufort;
}
String windDirectionIcon(int degrees) {
String directions[] = {"\uf044", "\uf088", "\uf04d", "\uf057", "\uf058", "\uf087", "\uf048", "\uf043"};
int index = (degrees + 22) / 45;
if (index >= 8) {
index = 0;
}
return directions[index];
}

View File

@@ -31,11 +31,11 @@ void setup() {
Serial.begin(115200);
Serial.print(">\n");
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org", "time.nist.gov");
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "0.nl.pool.ntp.org", "europe.pool.ntp.org", "time.nist.gov");
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
init_web();
loadDB("/tagDB.json");
loadDB("/current/tagDB.json");
xTaskCreate(timeTask, "timed tasks", 10000, NULL, 2, NULL);
xTaskCreate(zbsRxTask, "zbsRX Process", 10000, NULL, 2, NULL);

View File

@@ -4,6 +4,7 @@
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>
#include <makeimage.h>
#include <web.h>
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);
@@ -22,7 +23,7 @@ void jpg2grays(String filein, String fileout) {
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
Serial.println("Failed to create sprite in jpg2grays");
wsErr("Failed to create sprite in jpg2grays");
}
spr.setColorDepth(8);
spr.fillSprite(TFT_WHITE);

View File

@@ -71,12 +71,11 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
if (hdr.width == 296 && hdr.height == 128) {
//sorry, can't rotate
Serial.println("when using BMP files, remember to only use 128px width and 296px height");
wsString("when using BMP files, remember to only use 128px width and 296px height");
wsErr("when using BMP files, remember to only use 128px width and 296px height");
return false;
}
if (hdr.sig[0] == 'B' && hdr.sig[1] == 'M' && hdr.bpp == 24) {
Serial.println("converting 24bpp bmp to grays");
wsString("converting 24bpp bmp to grays");
char fileout[64];
sprintf(fileout, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]);
bmp2grays(*filename,(String)fileout);
@@ -88,7 +87,6 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
if (filename->endsWith(".jpg") || filename->endsWith(".JPG")) {
Serial.println("converting jpg to grays");
wsString("converting jpg to grays");
char fileout[64];
sprintf(fileout, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]);
jpg2grays(*filename, (String)fileout);
@@ -114,7 +112,7 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
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.");
Serial.println("new image is the same as current image. not updating tag.");
wsSendTaginfo(mac);
return false;
}
@@ -156,7 +154,7 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
}
dstfile.close();
wsString("new image pending: " + String(dst_path));
wsLog("new image pending: " + String(dst_path));
if (taginfo != nullptr) {
taginfo->pending = true;
taginfo->CheckinInMinPending = nextCheckin + 1;
@@ -164,7 +162,7 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
}
}
else {
Serial.println("firmware upload pending");
wsLog("firmware upload pending");
}
file.close();
@@ -211,7 +209,7 @@ void processBlockRequest(struct espBlockRequest* br) {
sendBlock(pd->data + (br->blockId * BLOCK_DATA_SIZE), len);
char buffer[64];
sprintf(buffer, "< Block Request received for MD5 %llu, block %d\n\0", br->ver, br->blockId);
wsString((String)buffer);
wsLog((String)buffer);
Serial.printf("<BlockId=%d\n", br->blockId);
}
@@ -220,7 +218,7 @@ void processXferComplete(struct espXferComplete* xfc) {
uint8_t src[8];
*((uint64_t*)src) = swap64(*((uint64_t*)xfc->src));
sprintf(buffer, "< %02X%02X%02X%02X%02X%02X reports xfer complete\n\0", src[2], src[3], src[4], src[5], src[6], src[7]);
wsString((String)buffer);
wsLog((String)buffer);
Serial.print(buffer);
uint8_t mac[6];
memcpy(mac, src + 2, sizeof(mac));
@@ -270,12 +268,12 @@ void processDataReq(struct espAvailDataReq* eadr) {
time_t now;
time(&now);
taginfo->lastseen = now;
//taginfo->model = eadr->adr.hwType;
taginfo->expectedNextCheckin = now + 300;
taginfo->button = (eadr->adr.wakeupReason==WAKEUP_REASON_GPIO);
Serial.printf("t=%d, lqi=%d, rssi=%d, ", eadr->adr.temperature, eadr->adr.lastPacketLQI, eadr->adr.lastPacketRSSI);
Serial.printf("hwtype=%d, reason=%d, volt=%d", eadr->adr.hwType,eadr->adr.wakeupReason,eadr->adr.batteryMv);
sprintf(buffer, "<ADR %02X%02X%02X%02X%02X%02X\n\0", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Serial.print(buffer);
//wsString((String)buffer);
wsSendTaginfo(mac);
}

View File

@@ -138,7 +138,6 @@ void SerialRXLoop() {
}
if (strncmp(cmdbuffer, "BST>", 4) == 0) {
Serial.print(">SYNC BURST\n");
wsString(">SYNC BURST");
RXState = ZBS_RX_WAIT_HEADER;
}
if (strncmp(cmdbuffer, "XFC>", 4) == 0) {

View File

@@ -143,14 +143,22 @@ void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType
}
}
void wsString(String text) {
DynamicJsonDocument doc(100);
void wsLog(String text) {
StaticJsonDocument<500> doc;
doc["logMsg"] = text;
xSemaphoreTake(wsMutex, portMAX_DELAY);
ws.textAll(doc.as<String>());
xSemaphoreGive(wsMutex);
}
void wsErr(String text) {
StaticJsonDocument<500> doc;
doc["errMsg"] = text;
xSemaphoreTake(wsMutex, portMAX_DELAY);
ws.textAll(doc.as<String>());
xSemaphoreGive(wsMutex);
}
void wsSendSysteminfo() {
DynamicJsonDocument doc(250);
JsonObject sys = doc.createNestedObject("sys");
@@ -181,11 +189,6 @@ void wsSendTaginfo(uint8_t mac[6]) {
void init_web() {
LittleFS.begin(true);
if (!LittleFS.exists("/.exclude.files")) {
Serial.println("littlefs exclude.files aanmaken");
File f = LittleFS.open("/.exclude.files", "w");
f.close();
}
if (!LittleFS.exists("/current")) {
LittleFS.mkdir("/current");
}
@@ -214,8 +217,9 @@ void init_web() {
ESP.restart();
});
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.htm");
server.serveStatic("/current", LittleFS, "/current/");
server.serveStatic("/", LittleFS, "/www/").setDefaultFile("index.html");
server.on(
"/imgupload", HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200);
@@ -282,7 +286,7 @@ void init_web() {
taginfo->model = atoi(request->getParam("model", true)->value().c_str());
taginfo->nextupdate = 0;
wsSendTaginfo(mac);
saveDB("/tagDB.json");
saveDB("/current/tagDB.json");
request->send(200, "text/plain", "Ok, saved");
} else {
request->send(200, "text/plain", "Error while saving: mac not found");