const repoUrl = 'https://api.github.com/repos/jjwbruijn/OpenEPaperLink/releases'; const $ = document.querySelector.bind(document); let running = false; let errors = 0; let env = '', currentVer = '', currentBuildtime = 0; let buttonState = false; export async function initUpdate() { if (!$("#updateconsole")) { const consoleDiv = document.createElement('div'); consoleDiv.classList.add('console'); consoleDiv.id = "updateconsole"; $('#apupdatebox').appendChild(consoleDiv); } $("#updateconsole").innerHTML = ""; const response = await fetch("/version.txt"); let filesystemversion = await response.text(); if (!filesystemversion) filesystemversion = "unknown"; 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) { let matchtest = ''; if (data.buildversion != filesystemversion && filesystemversion != "custom" && data.buildversion != "custom") matchtest = " <- not matching!" print(`env: ${data.env}`); print(`build date: ${formatEpoch(data.buildtime)}`); print(`esp32 version: ${data.buildversion}`); print(`filesystem version: ${filesystemversion}` + matchtest); print(`sha: ${data.sha}`); print(`psram size: ${data.psramsize}`); print(`flash size: ${data.flashsize}`); print("--------------------------", "gray"); env = data.env; currentVer = data.buildversion; currentBuildtime = data.buildtime; if (data.rollback) $("#rollbackOption").style.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; const filesJsonAsset = assets.find(asset => asset.name === 'filesystem.json'); const binariesJsonAsset = assets.find(asset => asset.name === 'binaries.json'); if (filesJsonAsset && binariesJsonAsset) { 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: filesJsonAsset.browser_download_url, bin_url: binariesJsonAsset.browser_download_url } }; }); const easyupdate = $('#easyupdate'); if (releaseDetails.length === 0) { easyupdate.innerHTML = ("No releases found."); } else { const release = releaseDetails[0]; if (release?.tag_name) { if (release.tag_name == currentVer) { easyupdate.innerHTML = `Version ${currentVer}. You are up to date`; } else if (release.date < formatEpoch(currentBuildtime)) { easyupdate.innerHTML = `Your version is newer than the latest release date.
Are you the developer? :-)`; } else { easyupdate.innerHTML = `An update from version ${currentVer} to version ${release.tag_name} is available.`; } } } easyupdate.innerHTML += "
advanced options" const table = document.createElement('table'); const tableHeader = document.createElement('tr'); tableHeader.innerHTML = 'ReleaseDateNameUpdate:Remark'; table.appendChild(tableHeader); releaseDetails.forEach(release => { if (release?.html_url) { const tableRow = document.createElement('tr'); let tablerow = `${release.tag_name}${release.date}${release.name}`; if (release.tag_name == currentVer) { tablerow += "current version"; } else if (release.date < formatEpoch(currentBuildtime)) { tablerow += "older"; } else { tablerow += "newer"; } tableRow.innerHTML = tablerow; table.appendChild(tableRow); } }); $('#releasetable').innerHTML = ""; $('#releasetable').appendChild(table); disableButtons(buttonState); }) .catch(error => { print('Error fetching releases:' + error, "red"); }); } export function updateAll(binUrl, fileUrl, tagname) { updateWebpage(fileUrl, tagname, false) .then(() => { updateESP(binUrl, false); }) .catch(error => { console.error(error); }); } export async function updateWebpage(fileUrl, tagname, showReload) { return new Promise((resolve, reject) => { (async function () { try { if (running) return; if (showReload) { if (!confirm("Confirm updating the filesystem")) return; } else { if (!confirm("Confirm updating the esp32 and filesystem")) return; } disableButtons(true); running = true; errors = 0; const consoleDiv = document.getElementById('updateconsole'); consoleDiv.scrollTop = consoleDiv.scrollHeight; print("Updating littleFS partition..."); fetch("https://openepaperlink.eu/getupdate/?url=" + fileUrl) .then(response => response.json()) .then(data => { checkfiles(data); }) .catch(error => { print('Error fetching data:' + error, "red"); }); const checkfiles = async (files) => { const updateactions = files.find(files => files.name === "update_actions.json"); if (updateactions) { await fetchAndPost(updateactions.url, updateactions.name, updateactions.path); try { const response = await fetch("/update_actions", { method: "POST", body: '' }); if (response.ok) { await response.text(); } else { print(`error performing update actions: ${response.status}`, "red"); errors++; } } catch (error) { console.error(`error calling update actions:` + error, "red"); errors++; } } for (const file of files) { try { if (file.name != "update_actions.json") { 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++; } } writeVersion(tagname, "version.txt", "/www/version.txt") running = false; if (errors) { print("------", "gray"); print(`Finished updating with ${errors} errors.`, "red"); reject(error); } else { print("------", "gray"); print("Update succesful."); resolve(); } disableButtons(false); if (showReload) { const newLine = document.createElement('div'); newLine.innerHTML = ""; consoleDiv.appendChild(newLine); consoleDiv.scrollTop = consoleDiv.scrollHeight; } }; } catch (error) { print('Error: ' + error, "red"); errors++; reject(error); } })(); }); } export async function updateESP(fileUrl, showConfirm) { if (running) return; if (showConfirm) { if (!confirm("Confirm updating the esp32")) return; } disableButtons(true); running = true; errors = 0; const consoleDiv = document.getElementById('updateconsole'); consoleDiv.scrollTop = consoleDiv.scrollHeight; print("Updating firmware..."); let binurl, binmd5, binsize; let retryCount = 0; const maxRetries = 5; while (retryCount < maxRetries) { try { const response = await fetch("https://openepaperlink.eu/getupdate/?url=" + fileUrl + "&env=" + env); const responseBody = await response.text(); if (!response.ok) { throw new Error("Network response was not OK: " + responseBody); } if (!responseBody.trim().startsWith("[")) { throw new Error("Failed to fetch the release info file"); } const data = JSON.parse(responseBody); const file = data.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) { 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"); } break; } else { print(`No info about "${env}" found in the release.`, "red"); } } catch (error) { print('Error: ' + error.message, "yellow"); retryCount++; print(`Retrying... attempt ${retryCount}`); await new Promise((resolve) => setTimeout(resolve, 3000)); } } if (retryCount === maxRetries) { print("Reached maximum retry count. Failed to execute the update.", "red"); } running = false; disableButtons(false); } $('#rollbackBtn').onclick = function () { if (running) return; disableButtons(true); running = true; errors = 0; const consoleDiv = document.getElementById('updateconsole'); consoleDiv.scrollTop = consoleDiv.scrollHeight; print("Rolling back..."); fetch("/rollback", { method: "POST", body: '' }) 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; if (line == "[reboot]") { newLine.innerHTML = ""; } else { newLine.textContent = line; } consoleDiv.appendChild(newLine); if (isScrolledToBottom) { consoleDiv.scrollTop = consoleDiv.scrollHeight; } } } export function reboot() { print("Rebooting now... Reloading webpage in 5 seconds...", "yellow"); fetch("/reboot", { method: "POST" }); setTimeout(() => { location.reload(); }, 5000); } 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 localTimeZoneOffset = new Date().getTimezoneOffset(); const date = new Date(utcDateString); date.setMinutes(date.getMinutes() - localTimeZoneOffset); 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++; } }; const writeVersion = async (content, name, path) => { try { print("uploading " + path); const formData = new FormData(); formData.append('path', path); const blob = new Blob([content]); formData.append('file', blob, 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; }