first version of OTA firmware updates

This commit is contained in:
Nic Limper
2023-05-27 19:25:28 +02:00
parent cd288e79f4
commit 3788608e63
17 changed files with 813 additions and 133 deletions

View File

@@ -112,7 +112,7 @@ Latency will be around 40 seconds.">
<p>
<span id="rebootbutton">reboot AP</span>
<a href="/backup_db" id="downloadDBbutton">download tagDB</a>
<!--<span id="updatebutton">update</span>-->
<span id="updatebutton">update</span>
</p>
<p>
<a href="https://github.com/jjwbruijn/OpenEPaperLink" target="_new">Github OpenEPaperLink</a>
@@ -121,21 +121,15 @@ Latency will be around 40 seconds.">
<div id="apupdatebox">
<div class="closebtn">&#10006;</div>
<h3>Firmware updates</h3>
<p>
test
</p>
<p>
<input type="button" value="Save" id="apfwsave">
</p>
<h3>Update dashboard</h3>
Updates are fetched directly from the Github repo.
<div id="releasetable"></div>
<div id="rollbackOption" style="display:none"><button id="rollbackBtn">Rollback to previous firmware</button></div>
<!--
<p><pre>
work in progress
- upload tagDB
- update webinterface from github
- update APtag FW from github
- update all tag FW from github
- update esp32 fw
</pre>
</p>
-->
@@ -149,6 +143,7 @@ Latency will be around 40 seconds.">
<div class="actionbox">
<div>
<div>Currently active tags:</div>
<div><span id="apstatecolor">&#11044;</span> <span id="apstate">loading</span></div>
<div><span id="apconfigbutton">AP config</span></div>
<div><a href="/edit" target="littlefs" class="filebutton">edit littleFS</a></div>
</div>

View File

@@ -70,6 +70,7 @@ label {
text-decoration: none;
color: black;
cursor: pointer;
white-space: nowrap;
}
.columns div {
@@ -112,8 +113,8 @@ select {
#configbox, #apconfigbox, #apupdatebox {
display: none;
position: fixed;
top: 80px;
left: 50px;
top: 65px;
left: 15px;
width: 380px;
padding: 15px;
background-color: #f0e6d3;
@@ -168,7 +169,10 @@ select {
}
#apupdatebox {
background-color: #f0e0d0;
background-color: #f0d0c8;
width: 700px;
padding-bottom: 20px;
border: 1px solid #d0b0a8;
}
#cfgdelete {
@@ -414,6 +418,54 @@ ul.messages li.new {
border: solid 1px #666666;
}
/* updatescreens */
#releasetable {
margin: 10px 0px;
}
#releasetable table {
border-spacing: 1px;
}
#releasetable th {
text-align: left;
background-color: #ffffff;
padding: 1px 5px;
}
#releasetable td {
background-color: #ffffff;
padding: 1px 5px;
min-width: 70px;
}
#releasetable button {
padding: 3px 10px;
background-color: #e0e0e0;
}
#releasetable button:hover {
background-color: #a0a0a0;
}
.console {
width: 100%;
background-color: black;
font-family: 'lucida console','ui-monospace';
color: white;
padding: 5px 10px;
margin: 20px 0px;
padding-bottom: 25px;
height: 400px;
overflow-y: scroll;
white-space: break-spaces;
}
.console div {
word-break: break-all;
}
/* media */
@media(max-width: 460px) {
.messages li div, ul.messages li div.date, ul.messages li div.message {
@@ -452,7 +504,7 @@ ul.messages li.new {
/* styles for mobile devices in portrait mode */
body {
font-size: 14px;
font-size: 13px;
}
.tagcard {
@@ -489,4 +541,7 @@ ul.messages li.new {
text-align: center;
}
}
.actionbox>div {
gap: 5px;
}
}

View File

@@ -15,26 +15,39 @@ const displaySizeLookup = { 0: [152, 152], 1: [128, 296], 2: [400, 300] };
displaySizeLookup[17] = [128, 296];
const colorTable = { 0: [255, 255, 255], 1: [0, 0, 0], 2: [255, 0, 0], 3: [150, 150, 150] };
const apstate = [
{ state: "offline", color: "red" },
{ state: "online", color: "green" },
{ state: "flashing", color: "orange" },
{ state: "wait for reset", color: "blue" },
{ state: "requires power cycle", color: "purple" },
{ state: "failed", color: "red" },
{ state: "coming online", color: "yellow" }
];
const imageQueue = [];
let isProcessing = false;
let servertimediff = 0;
let paintLoaded = false, paintShow = false;
var cardconfig;
let otamodule;
window.addEventListener("load", function () {
fetch("/get_ap_list")
fetch("/get_ap_config")
.then(response => response.json())
.then(data => {
if (data.alias) {
$(".logo").innerHTML = data.alias;
this.document.title = data.alias;
}
})
fetch('/content_cards.json')
});
fetch('/content_cards.json')
.then(response => response.json())
.then(data => {
cardconfig = data;
loadTags(0);
connect();
setInterval(updatecards, 1000);
})
.catch(error => {
console.error('Error:', error);
@@ -43,8 +56,6 @@ window.addEventListener("load", function () {
});
let socket;
connect();
setInterval(updatecards, 1000);
function loadTags(pos) {
fetch("/get_db?pos=" + pos)
@@ -77,6 +88,10 @@ function connect() {
}
if (msg.sys) {
$('#sysinfo').innerHTML = 'free heap: ' + msg.sys.heap + ' bytes &#x2507; db size: ' + msg.sys.dbsize + ' bytes &#x2507; db record count: ' + msg.sys.recordcount + ' &#x2507; littlefs free: ' + msg.sys.littlefsfree + ' bytes';
if (msg.sys.apstate) {
$("#apstatecolor").style.color = apstate[msg.sys.apstate].color;
$("#apstate").innerHTML = apstate[msg.sys.apstate].state;
}
servertimediff = (Date.now() / 1000) - msg.sys.currtime;
}
if (msg.apitem) {
@@ -87,6 +102,16 @@ function connect() {
row.insertCell(3).innerHTML = msg.apitem.channel;
row.insertCell(4).innerHTML = msg.apitem.version;
}
if (msg.console) {
console.log(otamodule);
if (otamodule && typeof(otamodule.print) === "function") {
let color = "#c0c0c0";
if (msg.console.startsWith("Fail") || msg.console.startsWith("Err")) {
color = "red";
}
otamodule.print(msg.console, color);
}
}
});
socket.addEventListener("close", (event) => {
@@ -346,7 +371,7 @@ $('#apconfigbutton').onclick = function () {
for (var i = rowCount - 1; i > 0; i--) {
table.deleteRow(i);
}
fetch("/get_ap_list")
fetch("/get_ap_config")
.then(response => response.json())
.then(data => {
$('#apcfgalias').value = data.alias;
@@ -381,7 +406,12 @@ $('#apcfgsave').onclick = function () {
$('#updatebutton').onclick = function () {
$('#apconfigbox').style.display = 'none';
$('#apupdatebox').style.display = 'block';
//https://api.github.com/repos/jjwbruijn/OpenEPaperLink/commits
loadOTA();
}
async function loadOTA() {
otamodule = await import('./ota.js?v=' + Date.now());
otamodule.initUpdate();
}
$('#paintbutton').onclick = function () {
@@ -578,3 +608,4 @@ function sortGrid() {
});
gridItems.forEach((item) => sortableGrid.appendChild(item));
}

View File

@@ -0,0 +1,292 @@
const repoUrl = 'https://api.github.com/repos/jonasniesner/OpenEPaperLink/releases';
const $ = document.querySelector.bind(document);
let running = false;
let errors = 0;
let env = '';
let buttonState = false;
export function initUpdate() {
if (!$("#updateconsole")) {
const consoleDiv = document.createElement('div');
consoleDiv.classList.add('console');
consoleDiv.id = "updateconsole";
$('#apupdatebox').appendChild(consoleDiv);
}
$("#updateconsole").innerHTML = "";
fetch("/sysinfo")
.then(response => {
if (response.status != 200) {
print("Error fetching sysinfo: " + response.status, "red");
if (response.status == 404) {
print("Your current firmware version is not yet capable of updating OTA.");
print("Update it manually one last time.");
disableButtons(true);
}
return "{}";
} else {
return response.json();
}
})
.then(data => {
if (data.env) {
print(`env: ${data.env}`);
print(`build date: ${formatEpoch(data.buildtime)}`);
print(`version: ${data.buildversion}`);
print(`sha: ${data.sha}`);
print(`psram size: ${data.psramsize}`);
print(`flash size: ${data.flashsize}`);
print("--------------------------","gray");
env = data.env;
if (data.rollback) $("#rollbackOption").display = 'block';
}
})
.catch(error => {
print('Error fetching sysinfo: ' + error, "red");
});
fetch(repoUrl)
.then(response => response.json())
.then(data => {
const releaseDetails = data.map(release => {
const assets = release.assets;
let fileUrl = null;
const filesJsonAsset = assets.find(asset => asset.name === 'files.json');
if (filesJsonAsset) {
fileUrl = filesJsonAsset.browser_download_url;
return {
html_url: release.html_url,
tag_name: release.tag_name,
name: release.name,
date: formatDateTime(release.published_at),
author: release.author.login,
file_url: fileUrl
}
};
});
const table = document.createElement('table');
const tableHeader = document.createElement('tr');
tableHeader.innerHTML = '<th>Release</th><th>Date</th><th>Name</th><th>Author</th><th colspan="2">Update:</th>';
table.appendChild(tableHeader);
releaseDetails.forEach(release => {
const tableRow = document.createElement('tr');
tableRow.innerHTML = `<td><a href="${release.html_url}" target="_new">${release.tag_name}</a></td><td>${release.date}</td><td>${release.name}</td><td>${release.author}</td><td><button onclick="otamodule.updateESP('${release.file_url}')">MCU</button></td><td><button onclick="otamodule.updateWebpage('${release.file_url}')">Filesystem</button></td>`;
table.appendChild(tableRow);
});
$('#releasetable').innerHTML = "";
$('#releasetable').appendChild(table);
disableButtons(buttonState);
})
.catch(error => {
print('Error fetching releases:' + error, "red");
});
}
export function updateWebpage(fileUrl) {
if (running) return;
if (!confirm("Confirm updating the littleFS storage")) return;
disableButtons(true);
running = true;
errors = 0;
consoleDiv.scrollTop = consoleDiv.scrollHeight;
print("Updating littleFS partition...");
fetch("/getexturl?url=" + fileUrl)
.then(response => response.json())
.then(data => {
checkfiles(data.files);
})
.catch(error => {
print('Error fetching data:' + error, "red");
});
const checkfiles = async (files) => {
for (const file of files) {
try {
const url = "/check_file?path=" + encodeURIComponent(file.path);
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
if (data.filesize == file.size && data.md5 == file.md5) {
print(`file ${file.path} is up to date`, "green");
} else if (data.filesize == 0) {
await fetchAndPost(file.url, file.name, file.path);
} else {
await fetchAndPost(file.url, file.name, file.path);
}
} else {
print(`error checking file ${file.path}: ${response.status}`, "red");
errors++;
}
} catch (error) {
console.error(`error checking file ${file.path}:` + error, "red");
errors++;
}
}
running = false;
if (errors) {
print("------", "gray");
print(`Finished updating with ${errors} errors.`, "red");
} else {
print("------", "gray");
print("Update succesfull.");
}
disableButtons(false);
};
}
export async function updateESP(fileUrl) {
if (running) return;
if (!confirm("Confirm updating the microcontroller")) return;
disableButtons(true);
running = true;
errors = 0;
consoleDiv.scrollTop = consoleDiv.scrollHeight;
print("Updating firmware...");
let binurl, binmd5, binsize;
try {
const response = await fetch("/getexturl?url=" + fileUrl);
const data = await response.json();
const file = data.binaries.find((entry) => entry.name == env + '.bin');
if (file) {
binurl = file.url;
binmd5 = file.md5;
binsize = file.size;
console.log(`URL for "${file.name}": ${binurl}`);
try {
const response = await fetch('/update_ota', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
url: binurl,
md5: binmd5,
size: binsize
})
});
if (response.ok) {
const result = await response.text();
print('OTA update initiated.');
} else {
print('Failed to initiate OTA update: ' + response.status, "red");
}
} catch (error) {
print('Error during OTA update: ' + error, "red");
}
} else {
print(`File "${fileName}" not found.`, "red");
}
} catch (error) {
print('Error: ' + error, "red");
}
running = false;
disableButtons(false);
}
$('#rollbackBtn').onclick = function () {
if (running) return;
if (!confirm("Confirm switching to previeous firmware")) return;
disableButtons(true);
running = true;
errors = 0;
consoleDiv.scrollTop = consoleDiv.scrollHeight;
print("Rolling back...");
fetch("/rollback", {
method: "POST",
body: formData
})
running = false;
disableButtons(false);
}
export function print(line, color = "white") {
const consoleDiv = document.getElementById('updateconsole');
if (consoleDiv) {
const isScrolledToBottom = consoleDiv.scrollHeight - consoleDiv.clientHeight <= consoleDiv.scrollTop;
const newLine = document.createElement('div');
newLine.style.color = color;
newLine.textContent = line;
consoleDiv.appendChild(newLine);
if (isScrolledToBottom) {
consoleDiv.scrollTop = consoleDiv.scrollHeight;
}
}
}
function formatEpoch(epochTime) {
const date = new Date(epochTime * 1000); // Convert seconds to milliseconds
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
function formatDateTime(utcDateString) {
const date = new Date(utcDateString);
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}`;
return formattedDate;
}
const fetchAndPost = async (url, name, path) => {
try {
print("updating " + path);
const response = await fetch(url);
const fileContent = await response.blob();
const formData = new FormData();
formData.append('path', path);
formData.append('file', fileContent, name);
const uploadResponse = await fetch('/littlefs_put', {
method: 'POST',
body: formData
});
if (!uploadResponse.ok) {
print(`${response.status} ${response.body}`, "red");
errors++;
}
} catch (error) {
print('error: ' + error, "red");
errors++;
}
};
function disableButtons(active) {
$("#apupdatebox").querySelectorAll('button').forEach(button => {
button.disabled = active;
});
buttonState = active;
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Image Upload Form</title>
</head>
<body>
<h3>demo upload form</h3>
<p>You can use this as an example how to push images to a tag by an external server/script.</p>
<p>
<form method="POST" enctype="multipart/form-data" action="/imgupload">
<p>
<label for="mac">Enter a 6 or 8 byte MAC address:</label><br>
<input type="text" id="mac" name="mac">
</p>
<p>
<label for="dither">Floyd Steinberg dithering (0=off, 1=on)</label><br>
<input type="text" id="dither" name="dither">
</p>
<p>
<label for="image">Select an image to upload. Must have the correct resolution for the tag (rotating is allowed):</label><br>
<input type="file" id="image" name="file">
</p>
<p>
<input type="submit" value="Upload">
</p>
</form>
</p>
</body>
</html>

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Image Upload Form</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="/imgupload">
<label for="mac">Enter a 6 or 8 byte MAC address:</label><br>
<input type="text" id="mac" name="mac"><br>
<label for="dither">Floyd Steinberg dithering (0=off, 1=on)</label><br>
<input type="text" id="dither" name="dither"><br>
<label for="image">Select an image to upload. Must have the correct resolution for the tag (rotating is allowed):</label><br>
<input type="file" id="image" name="file"><br>
<input type="submit" value="Upload">
</form>
</body>
</html>

View File

@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x150000,
app1, app, ota_1, 0x160000,0x150000,
spiffs, data, spiffs, 0x2B0000,0x140000,
coredump, data, coredump,0x3F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x150000
5 app1 app ota_1 0x160000 0x150000
6 spiffs data spiffs 0x2B0000 0x140000
7 coredump data coredump 0x3F0000 0x10000

View File

@@ -1,6 +1,5 @@
#include <Arduino.h>
#include <LittleFS.h>
#define DISABLE_ALL_LIBRARY_WARNINGS
#include <TFT_eSPI.h>
#include "makeimage.h"

View File

@@ -0,0 +1,12 @@
#include <Arduino.h>
#include "web.h"
void handleSysinfoRequest(AsyncWebServerRequest* request);
void handleCheckFile(AsyncWebServerRequest* request);
void handleGetExtUrl(AsyncWebServerRequest* request);
void handleLittleFSUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
void handleUpdateOTA(AsyncWebServerRequest* request);
void firmwareUpdateTask(void* parameter);
void updateFirmware(const char* url, const char* expectedMd5, size_t size);
void handleRollback(AsyncWebServerRequest* request);

View File

@@ -12,9 +12,9 @@ void wsErr(String text);
void wsSendTaginfo(uint8_t *mac, uint8_t syncMode);
void wsSendSysteminfo();
void wsSendAPitem(struct APlist* apitem);
void wsSerial(String text);
uint8_t wsClientCount();
extern AsyncWebSocket ws;
extern SemaphoreHandle_t wsMutex;
extern TaskHandle_t websocketUpdater;

View File

@@ -29,10 +29,6 @@ board_build.f_cpu = 240000000L
;upload_port = COM30
;monitor_port = COM30
build_flags =
-D BUILD_ENV_NAME=$PIOENV
-D BUILD_TIME=$UNIX_TIME
[env:OpenEPaperLink_Mini_AP]
platform = https://github.com/platformio/platform-espressif32.git
board=lolin_s2_mini
@@ -40,6 +36,8 @@ board_build.partitions = default.csv
build_unflags =
-D CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y
build_flags =
-D BUILD_ENV_NAME=$PIOENV
-D BUILD_TIME=$UNIX_TIME
-D OPENEPAPERLINK_MINI_AP_PCB
-D ARDUINO_USB_MODE=0
-D CONFIG_SPIRAM_USE_MALLOC=1
@@ -62,6 +60,11 @@ build_flags =
-D FLASHER_LED=15
-D FLASHER_RGB_LED=33
-D USER_SETUP_LOADED
-D DISABLE_ALL_LIBRARY_WARNINGS
-D ILI9341_DRIVER
-D SMOOTH_FONT
-D LOAD_FONT2
build_src_filter =
+<*>-<usbflasher.cpp>-<serialconsole.cpp>
@@ -86,6 +89,8 @@ build_unflags =
-D ARDUINO_USB_MODE=1
-D CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y
build_flags =
-D BUILD_ENV_NAME=$PIOENV
-D BUILD_TIME=$UNIX_TIME
-D OPENEPAPERLINK_PCB
-D ARDUINO_USB_MODE=0
-D CONFIG_ESP32S3_SPIRAM_SUPPORT=1
@@ -127,6 +132,12 @@ build_flags =
-D FLASHER_LED=21
-D FLASHER_RGB_LED=48
-D USER_SETUP_LOADED
-D DISABLE_ALL_LIBRARY_WARNINGS
-D ILI9341_DRIVER
-D SMOOTH_FONT
-D LOAD_FONT2
board_build.flash_mode=qio
board_build.arduino.memory_type = qio_opi
board_build.psram_type=qspi_opi
@@ -140,6 +151,8 @@ board = esp32dev
board_build.partitions = default.csv
build_flags =
-D BUILD_ENV_NAME=$PIOENV
-D BUILD_TIME=$UNIX_TIME
-D CORE_DEBUG_LEVEL=0
-D SIMPLE_AP
@@ -153,9 +166,14 @@ build_flags =
-D FLASHER_AP_TEST=-1
-D FLASHER_AP_TXD=17
-D FLASHER_AP_RXD=16
-D FLASHER_LED=22
-D USER_SETUP_LOADED
-D DISABLE_ALL_LIBRARY_WARNINGS
-D ILI9341_DRIVER
-D SMOOTH_FONT
-D LOAD_FONT2
build_src_filter =
+<*>-<usbflasher.cpp>-<serialconsole.cpp>
@@ -168,6 +186,8 @@ board = esp32dev
board_build.partitions = no_ota.csv
build_flags =
-D BUILD_ENV_NAME=$PIOENV
-D BUILD_TIME=$UNIX_TIME
-D ALTERNATIVE_PCB
-D FLASHER_AP_SS=22
-D FLASHER_AP_CLK=13

View File

@@ -30,9 +30,12 @@
#include "web.h"
#include "language.h"
#define PAL_BLACK 0
#define PAL_WHITE 9
#define PAL_RED 2
// #define PAL_BLACK 0
// #define PAL_WHITE 9
// #define PAL_RED 2
#define PAL_BLACK TFT_BLACK
#define PAL_WHITE TFT_WHITE
#define PAL_RED TFT_RED
enum contentModes {
Image,
@@ -105,7 +108,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
imgParam imageParams;
imageParams.hasRed = false;
imageParams.dataType = DATATYPE_IMG_RAW_1BPP;
imageParams.dither = true;
imageParams.dither = false;
if (taginfo->hasCustomLUT) imageParams.grayLut = true;
imageParams.invert = false;
@@ -115,7 +118,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
case Image:
if (cfgobj["filename"].as<String>() && cfgobj["filename"].as<String>() != "null" && !cfgobj["#fetched"].as<bool>()) {
if (cfgobj["dither"] && cfgobj["dither"] == "0") imageParams.dither = false;
if (cfgobj["dither"] && cfgobj["dither"] == "1") imageParams.dither = true;
jpg2buffer(cfgobj["filename"].as<String>(), filename, imageParams);
if (imageParams.hasRed) imageParams.dataType = DATATYPE_IMG_RAW_2BPP;
if (prepareDataAvail(&filename, imageParams.dataType, mac, cfgobj["timetolive"].as<int>())) {
@@ -283,13 +286,16 @@ void drawString(TFT_eSprite &spr, String content, uint16_t posx, uint16_t posy,
}
void initSprite(TFT_eSprite &spr, int w, int h) {
spr.setColorDepth(4); // 4 bits per pixel, uses indexed color
// spr.setColorDepth(4); // 4 bits per pixel, uses indexed color
spr.setColorDepth(8);
spr.createSprite(w, h);
/*
uint16_t cmap[16];
cmap[PAL_BLACK] = TFT_BLACK;
cmap[PAL_RED] = TFT_RED;
cmap[PAL_WHITE] = TFT_WHITE;
spr.createPalette(cmap, 16);
*/
if (spr.getPointer() == nullptr) {
wsErr("Failed to create sprite");
}
@@ -815,7 +821,7 @@ bool getRssFeed(String &filename, String URL, String title, tagRecord *&taginfo,
struct tm timeInfo;
char header[32];
getLocalTime(&timeInfo);
sprintf(header, "%02d-%02d-%04d %02d:%02d", timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900, timeInfo.tm_hour, timeInfo.tm_min);
//sprintf(header, "%02d-%02d-%04d %02d:%02d", timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900, timeInfo.tm_hour, timeInfo.tm_min);
const char *url = URL.c_str();
const char *tag = "title";
@@ -1161,7 +1167,7 @@ void prepareNFCReq(uint8_t *dst, const char *url) {
void prepareLUTreq(uint8_t *dst, String input) {
const char *delimiters = ", \t";
const int maxValues = 70;
const int maxValues = 76;
uint8_t waveform[maxValues];
char *ptr = strtok(const_cast<char *>(input.c_str()), delimiters);
int i = 0;

View File

@@ -27,12 +27,12 @@ void timeTask(void* parameter) {
if (!getLocalTime(&tm)) {
Serial.println("Waiting for valid time from NTP-server");
} else {
if (now % 5 == 0) {
if (now % 5 == 0 || apInfo.state != AP_STATE_ONLINE) {
wsSendSysteminfo();
}
if (now % 300 == 6) saveDB("/current/tagDB.json");
contentRunner();
if (apInfo.isOnline) contentRunner();
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
@@ -76,6 +76,7 @@ void setup() {
heap_caps_malloc_extmem_enable(64);
#endif
/*
Serial.println("\n\n##################################");
Serial.printf("Internal Total heap %d, internal Free Heap %d\n", ESP.getHeapSize(), ESP.getFreeHeap());
Serial.printf("SPIRam Total heap %d, SPIRam Free Heap %d\n", ESP.getPsramSize(), ESP.getFreePsram());
@@ -99,6 +100,7 @@ void setup() {
p->type, p->subtype, p->address, p->size, p->label);
} while (pi = (esp_partition_next(pi)));
}
*/
#ifdef HAS_USB
// We'll need to start the 'usbflasher' task for boards with a second (USB) port. This can be used as a 'flasher' interface, using a python script on the host

View File

@@ -30,7 +30,7 @@ void jpg2buffer(String filein, String fileout, imgParam &imageParams) {
}
Serial.println("jpeg conversion " + String(w) + "x" + String(h));
spr.setColorDepth(8);
spr.setColorDepth(16);
spr.createSprite(w, h);
if (spr.getPointer() == nullptr) {
//no heap space for 8bpp, fallback to 1bpp
@@ -62,32 +62,11 @@ struct Error {
float b;
};
// Gamma brightness lookup table <https://victornpb.github.io/gamma-table-generator>
// gamma = 1.50 steps = 256 range = 0-255
const uint8_t gamma_lut[256] PROGMEM = {
0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4,
4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11,
11, 12, 12, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20,
21, 21, 22, 23, 23, 24, 25, 26, 26, 27, 28, 28, 29, 30, 31, 31,
32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 44,
45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 58,
59, 60, 61, 62, 63, 64, 65, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 102, 103, 104, 105, 106, 107,
108, 109, 110, 112, 113, 114, 115, 116, 117, 119, 120, 121, 122, 123, 124, 126,
127, 128, 129, 130, 132, 133, 134, 135, 136, 138, 139, 140, 141, 142, 144, 145,
146, 147, 149, 150, 151, 152, 154, 155, 156, 158, 159, 160, 161, 163, 164, 165,
167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 180, 181, 182, 184, 185, 187,
188, 189, 191, 192, 193, 195, 196, 197, 199, 200, 202, 203, 204, 206, 207, 209,
210, 211, 213, 214, 216, 217, 218, 220, 221, 223, 224, 226, 227, 228, 230, 231,
233, 234, 236, 237, 239, 240, 242, 243, 245, 246, 248, 249, 251, 252, 254, 255,
};
uint32_t colorDistance(const Color &c1, const Color &c2, const Error &e1) {
float r_diff = gamma_lut[c1.r] + e1.r - gamma_lut[c2.r];
float g_diff = gamma_lut[c1.g] + e1.g - gamma_lut[c2.g];
float b_diff = gamma_lut[c1.b] + e1.b - gamma_lut[c2.b];
return round(0.26 * r_diff * r_diff + 0.70 * g_diff * g_diff + 0.04 * b_diff * b_diff);
int32_t r_diff = c1.r + e1.r - c2.r;
int32_t g_diff = c1.g + e1.g - c2.g;
int32_t b_diff = c1.b + e1.b - c2.b;
return 3 * r_diff * r_diff + 6 * g_diff * g_diff + 1 * b_diff * b_diff;
}
void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
@@ -117,14 +96,14 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
{255, 0, 0} // Red
};
if (imageParams.grayLut) {
Color newColor = {150, 150, 150};
Color newColor = {160, 160, 160};
palette.push_back(newColor);
Serial.println("rendering with gray");
}
int num_colors = palette.size();
Color color;
Error *error_bufferold = new Error[bufw];
Error *error_buffernew = new Error[bufw];
Error *error_bufferold = new Error[bufw + 4];
Error *error_buffernew = new Error[bufw + 4];
memset(error_bufferold, 0, bufw * sizeof(Error));
for (uint16_t y = 0; y < bufh; y++) {
@@ -146,7 +125,7 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
}
}
uint16_t bitIndex = 7 - (x % 8);
uint8_t bitIndex = 7 - (x % 8);
uint16_t byteIndex = (y * bufw + x) / 8;
// this looks a bit ugly, but it's performing better than shorter notations
@@ -164,46 +143,42 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
imageParams.hasRed = true;
break;
}
/*
alt 1:
if (best_color_index & 1) {
blackBuffer[byteIndex] |= (1 << bitIndex);
}
if (best_color_index & 2) {
imageParams.hasRed = true;
redBuffer[byteIndex] |= (1 << bitIndex);
}
alt 2:
blackBuffer[byteIndex] |= ((best_color_index & 1) << bitIndex);
redBuffer[byteIndex] |= ((best_color_index & 2) << bitIndex);
imageParams.hasRed |= (best_color_index & 2);
*/
if (imageParams.dither) {
Error error = {
((float)color.r + error_bufferold[x].r - palette[best_color_index].r) / 16.0f,
((float)color.g + error_bufferold[x].g - palette[best_color_index].g) / 16.0f,
((float)color.b + error_bufferold[x].b - palette[best_color_index].b) / 16.0f};
static_cast<float>(color.r) + error_bufferold[x].r - static_cast<float>(palette[best_color_index].r),
static_cast<float>(color.g) + error_bufferold[x].g - static_cast<float>(palette[best_color_index].g),
static_cast<float>(color.b) + error_bufferold[x].b - static_cast<float>(palette[best_color_index].b) };
error_buffernew[x].r += error.r * 5.0f;
error_buffernew[x].g += error.g * 5.0f;
error_buffernew[x].b += error.b * 5.0f;
// Burkes Dithering
error_buffernew[x].r += error.r / 4.0f;
error_buffernew[x].g += error.g / 4.0f;
error_buffernew[x].b += error.b / 4.0f;
if (x > 0) {
error_buffernew[x - 1].r += error.r * 3.0f;
error_buffernew[x - 1].g += error.g * 3.0f;
error_buffernew[x - 1].b += error.b * 3.0f;
error_buffernew[x - 1].r += error.r / 8.0f;
error_buffernew[x - 1].g += error.g / 8.0f;
error_buffernew[x - 1].b += error.b / 8.0f;
}
if (x < bufw - 1) {
error_buffernew[x + 1].r += error.r * 1.0f;
error_buffernew[x + 1].g += error.g * 1.0f;
error_buffernew[x + 1].b += error.b * 1.0f;
error_bufferold[x + 1].r += error.r * 7.0f;
error_bufferold[x + 1].g += error.g * 7.0f;
error_bufferold[x + 1].b += error.b * 7.0f;
if (x > 1) {
error_buffernew[x - 2].r += error.r / 16.0f;
error_buffernew[x - 2].g += error.g / 16.0f;
error_buffernew[x - 2].b += error.b / 16.0f;
}
error_buffernew[x + 1].r += error.r / 8.0f;
error_buffernew[x + 1].g += error.g / 8.0f;
error_buffernew[x + 1].b += error.b / 8.0f;
error_bufferold[x + 1].r += error.r / 4.0f;
error_bufferold[x + 1].g += error.g / 4.0f;
error_bufferold[x + 1].b += error.b / 4.0f;
error_buffernew[x + 2].r += error.r / 16.0f;
error_buffernew[x + 2].g += error.g / 16.0f;
error_buffernew[x + 2].b += error.b / 16.0f;
error_bufferold[x + 2].r += error.r / 8.0f;
error_bufferold[x + 2].g += error.g / 8.0f;
error_bufferold[x + 2].b += error.b / 8.0f;
}
}
memcpy(error_bufferold, error_buffernew, bufw * sizeof(Error));

View File

@@ -0,0 +1,244 @@
#include "ota.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <FS.h>
#include <HTTPClient.h>
#include <LittleFS.h>
#include <MD5Builder.h>
#include <Update.h>
#include "tag_db.h"
#include "web.h"
#ifndef BUILD_ENV_NAME
#define BUILD_ENV_NAME unknown
#endif
#ifndef BUILD_TIME
#define BUILD_TIME 0
#endif
#ifndef BUILD_VERSION
#define BUILD_VERSION custom
#endif
#ifndef SHA
#define SHA 0
#endif
#define STR_IMPL(x) #x
#define STR(x) STR_IMPL(x)
void handleSysinfoRequest(AsyncWebServerRequest* request) {
StaticJsonDocument<250> doc;
doc["alias"] = config.alias;
doc["env"] = STR(BUILD_ENV_NAME);
doc["buildtime"] = STR(BUILD_TIME);
doc["buildversion"] = STR(BUILD_VERSION);
doc["sha"] = STR(SHA);
doc["psramsize"] = ESP.getPsramSize();
doc["flashsize"] = ESP.getFlashChipSize();
doc["rollback"] = Update.canRollBack();
size_t bufferSize = measureJson(doc) + 1;
AsyncResponseStream *response = request->beginResponseStream("application/json", bufferSize);
serializeJson(doc, *response);
request->send(response);
};
void handleCheckFile(AsyncWebServerRequest *request) {
if (!request->hasParam("path")) {
request->send(400);
return;
}
String filePath = request->getParam("path")->value();
File file = LittleFS.open(filePath, "r");
if (!file) {
StaticJsonDocument<64> doc;
doc["filesize"] = 0;
doc["md5"] = "";
String jsonResponse;
serializeJson(doc, jsonResponse);
request->send(200, "application/json", jsonResponse);
return;
}
size_t fileSize = file.size();
MD5Builder md5;
md5.begin();
md5.addStream(file, fileSize);
md5.calculate();
String md5Hash = md5.toString();
file.close();
StaticJsonDocument<128> doc;
doc["filesize"] = fileSize;
doc["md5"] = md5Hash;
String jsonResponse;
serializeJson(doc, jsonResponse);
request->send(200, "application/json", jsonResponse);
}
void handleGetExtUrl(AsyncWebServerRequest* request) {
if (request->hasParam("url")) {
String url = request->getParam("url")->value();
HTTPClient http;
http.begin(url);
http.setConnectTimeout(4000);
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
int httpResponseCode = http.GET();
if (httpResponseCode > 0) {
Serial.println(httpResponseCode);
String contentType = http.header("Content-Type");
size_t contentLength = http.getSize();
if (contentLength > 0) {
String content = http.getString();
AsyncWebServerResponse* response = request->beginResponse(200, contentType, content);
request->send(response);
} else {
request->send(500, "text/plain", "no size header");
}
} else {
request->send(httpResponseCode, "text/plain", "Failed to fetch URL");
}
http.end();
} else {
request->send(400, "text/plain", "Missing 'url' parameter");
}
}
void handleLittleFSUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
bool error = false;
if (!index) {
String path;
if (!request->hasParam("path", true)) {
path = "/temp/null.bin";
final = true;
error = true;
} else {
path = request->getParam("path", true)->value();
Serial.println("update " + path);
request->_tempFile = LittleFS.open(path, "w", true);
}
}
if (len) {
if (!request->_tempFile.write(data, len)) {
error = true;
final = true;
}
}
if (final) {
request->_tempFile.close();
if (error) {
request->send(507, "text/plain", "Error. Disk full?");
} else {
request->send(200, "text/plain", "Ok, file written");
}
}
}
struct FirmwareUpdateParams {
String url;
String md5;
size_t size;
};
void handleUpdateOTA(AsyncWebServerRequest* request) {
if (request->hasParam("url", true) && request->hasParam("md5", true) && request->hasParam("size", true)) {
saveDB("/current/tagDB.json");
FirmwareUpdateParams* params = new FirmwareUpdateParams;
params->url = request->getParam("url", true)->value();
params->md5 = request->getParam("md5", true)->value();
params->size = request->getParam("size", true)->value().toInt();
xTaskCreatePinnedToCore(firmwareUpdateTask, "OTAUpdateTask", 8192, params, 1, NULL, 1);
request->send(200, "text/plain", "In progress");
} else {
request->send(400, "Bad request");
}
}
void firmwareUpdateTask(void* parameter) {
FirmwareUpdateParams* params = reinterpret_cast<FirmwareUpdateParams*>(parameter);
const char* url = params->url.c_str();
const char* md5 = params->md5.c_str();
size_t size = params->size;
updateFirmware(url, md5, size);
delete params;
vTaskDelete(NULL);
}
void updateFirmware(const char* url, const char* expectedMd5, size_t size) {
HTTPClient httpClient;
wsSerial("start downloading");
wsSerial(url);
httpClient.begin(url);
httpClient.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
int httpCode = httpClient.GET();
if (httpCode == HTTP_CODE_OK) {
if (Update.begin(size)) {
Update.setMD5(expectedMd5);
unsigned long progressTimer = millis();
Update.onProgress([&progressTimer](size_t progress, size_t total) {
if (millis() - progressTimer > 500 || progress == total) {
char buffer[50];
sprintf(buffer, "Progress: %u%% %d %d\r\n", progress * 100 / total, progress, total);
wsSerial(String(buffer));
progressTimer = millis();
}
});
size_t written = Update.writeStream(httpClient.getStream());
if (written == httpClient.getSize()) {
if (Update.end(true)) {
wsSerial("Firmware update successful");
wsSerial("Restarting system...");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP.restart();
} else {
wsSerial("Error updating firmware:");
wsSerial(Update.errorString());
}
} else {
wsSerial("Error writing firmware data:");
wsSerial(Update.errorString());
}
} else {
wsSerial("Failed to begin firmware update");
wsSerial(Update.errorString());
}
} else {
wsSerial("Failed to download firmware file (HTTP code " + String(httpCode) + ")");
}
httpClient.end();
}
void handleRollback(AsyncWebServerRequest* request) {
if (Update.canRollBack()) {
bool rollbackSuccess = Update.rollBack();
if (rollbackSuccess) {
request->send(200, "Rollback successfull");
wsSerial("Rollback successfull");
wsSerial("Restarting system...");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP.restart();
} else {
wsSerial("Rollback failed");
request->send(400, "Rollback failed");
}
} else {
wsSerial("Rollback not allowed");
request->send(400, "Rollback not allowed");
}
}

View File

@@ -256,7 +256,7 @@ void initAPconfig() {
}
configFile.close();
}
config.channel = APconfig["channel"] | 25;
config.channel = APconfig["channel"] | 0;
if (APconfig["alias"]) strlcpy(config.alias, APconfig["alias"], sizeof(config.alias));
config.led = APconfig["led"] | 255;
config.language = APconfig["language"] | getDefaultLanguage();

View File

@@ -15,6 +15,8 @@
#include "language.h"
#include "leds.h"
#include "newproto.h"
#include "ota.h"
#include "serialap.h"
#include "settings.h"
#include "tag_db.h"
#include "udp.h"
@@ -27,10 +29,8 @@ AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
SemaphoreHandle_t wsMutex;
TaskHandle_t websocketUpdater;
void webSocketSendProcess(void *parameter) {
websocketUpdater = xTaskGetCurrentTaskHandle();
wsMutex = xSemaphoreCreateMutex();
while (true) {
ws.cleanupClients();
@@ -45,7 +45,6 @@ void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType
switch (type) {
case WS_EVT_CONNECT:
ets_printf("ws[%s][%u] connect\n", server->url(), client->id());
xTaskNotify(websocketUpdater, 2, eSetBits);
// client->ping();
break;
case WS_EVT_DISCONNECT:
@@ -59,6 +58,7 @@ void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType
ets_printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
break;
case WS_EVT_DATA:
/*
AwsFrameInfo *info = (AwsFrameInfo *)arg;
if (info->final && info->index == 0 && info->len == len) {
// the whole message is in a single frame and we got all of it's data
@@ -105,13 +105,13 @@ void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType
client->binary("{\"status\":\"received\"}");
}
}
}
} */
break;
}
}
void wsLog(String text) {
StaticJsonDocument<500> doc;
StaticJsonDocument<250> doc;
doc["logMsg"] = text;
if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY);
ws.textAll(doc.as<String>());
@@ -119,7 +119,7 @@ void wsLog(String text) {
}
void wsErr(String text) {
StaticJsonDocument<500> doc;
StaticJsonDocument<250> doc;
doc["errMsg"] = text;
if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY);
ws.textAll(doc.as<String>());
@@ -127,7 +127,7 @@ void wsErr(String text) {
}
void wsSendSysteminfo() {
DynamicJsonDocument doc(250);
DynamicJsonDocument doc(150);
JsonObject sys = doc.createNestedObject("sys");
time_t now;
time(&now);
@@ -136,6 +136,7 @@ void wsSendSysteminfo() {
sys["recordcount"] = tagDB.size();
sys["dbsize"] = tagDB.size() * sizeof(tagRecord);
sys["littlefsfree"] = LittleFS.totalBytes() - LittleFS.usedBytes();
sys["apstate"] = apInfo.state;
xSemaphoreTake(wsMutex, portMAX_DELAY);
ws.textAll(doc.as<String>());
@@ -199,6 +200,15 @@ void wsSendAPitem(struct APlist *apitem) {
if (wsMutex) xSemaphoreGive(wsMutex);
}
void wsSerial(String text) {
StaticJsonDocument<250> doc;
doc["console"] = text;
Serial.print(text);
if (wsMutex) xSemaphoreTake(wsMutex, portMAX_DELAY);
ws.textAll(doc.as<String>());
if (wsMutex) xSemaphoreGive(wsMutex);
}
uint8_t wsClientCount() {
return ws.count();
}
@@ -214,6 +224,7 @@ void init_web() {
}
WiFi.mode(WIFI_STA);
WiFiManager wm;
bool res;
res = wm.autoConnect("OpenEPaperLink Setup");
@@ -328,7 +339,7 @@ void init_web() {
}
});
server.on("/get_ap_list", HTTP_GET, [](AsyncWebServerRequest *request) {
server.on("/get_ap_config", HTTP_GET, [](AsyncWebServerRequest *request) {
UDPcomm udpsync;
udpsync.getAPList();
File configFile = LittleFS.open("/current/apconfig.json", "r");
@@ -378,6 +389,20 @@ void init_web() {
file.close();
});
server.on("/sysinfo", HTTP_GET, handleSysinfoRequest);
server.on("/check_file", HTTP_GET, handleCheckFile);
server.on("/getexturl", HTTP_GET, handleGetExtUrl);
server.on("/update_ota", HTTP_POST, [](AsyncWebServerRequest *request) {
handleUpdateOTA(request);
});
server.on("/rollback", HTTP_POST, handleRollback);
server.on(
"/littlefs_put", HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200);
},
handleLittleFSUpload);
server.onNotFound([](AsyncWebServerRequest *request) {
if (request->url() == "/" || request->url() == "index.htm") {
request->send(200, "text/html", "index.html not found. Did you forget to upload the littlefs partition?");