mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-04 15:34:19 +01:00
228 lines
5.5 KiB
JavaScript
228 lines
5.5 KiB
JavaScript
import { notifyMessage } from './errors.mjs';
|
|
import { listenAppConnected } from './connection.mjs';
|
|
import { variableListeners } from './settings.mjs';
|
|
|
|
let __free_size = 0;
|
|
|
|
/** @param {function(HTMLInputElement): void} callback */
|
|
function withUpgrade(callback) {
|
|
callback(/** @type {!HTMLInputElement} */
|
|
(document.querySelector("input[name=upgrade]")));
|
|
}
|
|
|
|
/** @param {function(HTMLProgressElement): void} callback */
|
|
function withProgress(callback) {
|
|
callback(/** @type {!HTMLProgressElement} */
|
|
(document.querySelector("progress#upgrade-progress")));
|
|
}
|
|
|
|
/**
|
|
* @param {number} flash_mode
|
|
* @returns string
|
|
*/
|
|
function describeFlashMode(flash_mode) {
|
|
if ((0 <= flash_mode) && (flash_mode <= 3)) {
|
|
const modes = ['QIO', 'QOUT', 'DIO', 'DOUT'];
|
|
return `${modes[flash_mode]} (${flash_mode})`;
|
|
}
|
|
|
|
return `Unknown flash mode ${flash_mode}`;
|
|
}
|
|
|
|
/**
|
|
* @param {Uint8Array} buffer
|
|
* @returns boolean
|
|
*/
|
|
function isGzip(buffer) {
|
|
return (0x1f === buffer[0]) && (0x8b === buffer[1]);
|
|
}
|
|
|
|
/**
|
|
* @param {Uint8Array} buffer
|
|
* @returns number
|
|
*/
|
|
function flashMode(buffer) {
|
|
return buffer[2];
|
|
}
|
|
|
|
/**
|
|
* @param {Uint8Array} buffer
|
|
* @returns boolean
|
|
*/
|
|
function checkMagic(buffer) {
|
|
return 0xe9 === buffer[0];
|
|
}
|
|
|
|
/**
|
|
* @param {Uint8Array} buffer
|
|
* @returns boolean
|
|
*/
|
|
function checkFlashMode(buffer) {
|
|
return 0x03 === flashMode(buffer);
|
|
}
|
|
|
|
/**
|
|
* @param {Event} event
|
|
*/
|
|
function notifyValueError(event) {
|
|
notifyMessage(`ERROR while attempting OTA upgrade - XHR ${event.type}`);
|
|
}
|
|
|
|
/**
|
|
* @param {Event} event
|
|
*/
|
|
function onButtonClick(event) {
|
|
event.preventDefault();
|
|
|
|
if (!(event.target instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
const url = event.target.dataset["url"];
|
|
if (!url) {
|
|
alert("Not connected");
|
|
return;
|
|
}
|
|
|
|
withUpgrade((upgrade) => {
|
|
const files = upgrade.files;
|
|
if (!files) {
|
|
alert("No file selected");
|
|
return;
|
|
}
|
|
|
|
const data = new FormData();
|
|
data.append("upgrade", files[0], files[0].name);
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.addEventListener("error", notifyValueError, false);
|
|
xhr.addEventListener("abort", notifyValueError, false);
|
|
|
|
xhr.addEventListener("load",
|
|
() => {
|
|
if ("OK" === xhr.responseText) {
|
|
alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds");
|
|
} else {
|
|
alert(`ERROR while attempting OTA upgrade - ${xhr.status.toString()} ${xhr.statusText}, ${xhr.responseText}`);
|
|
}
|
|
}, false);
|
|
|
|
withProgress((progress) => {
|
|
xhr.addEventListener("load",
|
|
() => {
|
|
progress.style.display = "none";
|
|
});
|
|
xhr.upload.addEventListener("progress",
|
|
(event) => {
|
|
progress.style.display = "inherit";
|
|
if (event.lengthComputable) {
|
|
progress.value = event.loaded;
|
|
progress.max = event.total;
|
|
}
|
|
}, false);
|
|
});
|
|
|
|
xhr.open("POST", url);
|
|
xhr.send(data);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {number} size
|
|
* @returns {number}
|
|
*/
|
|
function roundedSize(size) {
|
|
return (size - (size % 4096)) + 4096;
|
|
}
|
|
|
|
/**
|
|
* @param {Event} event
|
|
*/
|
|
async function onFileChanged(event) {
|
|
event.preventDefault();
|
|
|
|
const button = document.querySelector(".button-upgrade");
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
button.disabled = true;
|
|
|
|
if (!(event.target instanceof HTMLInputElement)) {
|
|
return;
|
|
}
|
|
|
|
if (!event.target.files || !event.target.files.length) {
|
|
return;
|
|
}
|
|
|
|
const file = event.target.files[0];
|
|
|
|
const filename = document.querySelector("input[name=filename]");
|
|
if (filename instanceof HTMLInputElement) {
|
|
filename.value = file.name;
|
|
}
|
|
|
|
const need = roundedSize(file.size);
|
|
if ((__free_size !== 0) && (need > __free_size)) {
|
|
alert(`OTA .bin cannot be uploaded. Need at least ${need}bytes of free space, ${__free_size}bytes available.`);
|
|
return;
|
|
}
|
|
|
|
const buffer = await file.slice(0, 3).arrayBuffer();
|
|
const header = new Uint8Array(buffer);
|
|
|
|
if (!isGzip(header)) {
|
|
if (!checkMagic(header)) {
|
|
alert("Invalid binary header, does not look like firmware .bin");
|
|
return;
|
|
}
|
|
|
|
if (!checkFlashMode(header)) {
|
|
alert(describeFlashMode(flashMode(header)));
|
|
}
|
|
}
|
|
|
|
button.disabled = false;
|
|
}
|
|
|
|
/**
|
|
* @returns {import('./settings.mjs').KeyValueListeners}
|
|
*/
|
|
function listeners() {
|
|
return {
|
|
"free_size": (_, value) => {
|
|
__free_size = parseInt(value, 10);
|
|
},
|
|
};
|
|
}
|
|
|
|
export function init() {
|
|
variableListeners(listeners());
|
|
|
|
const [upgrade] = document.querySelectorAll("input[name=upgrade]");
|
|
if (!(upgrade instanceof HTMLInputElement)) {
|
|
return;
|
|
}
|
|
|
|
const [browse] = document.querySelectorAll(".button-upgrade-browse")
|
|
browse.addEventListener("click", () => {
|
|
upgrade.click();
|
|
});
|
|
|
|
upgrade.addEventListener("change", onFileChanged);
|
|
|
|
const button = document.querySelector(".button-upgrade");
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
listenAppConnected((urls) => {
|
|
button.dataset["url"] = urls.upgrade.href;
|
|
});
|
|
|
|
button.addEventListener("click", onButtonClick);
|
|
button.disabled = true;
|
|
}
|