Files
espurna/code/html/src/ota.mjs
2024-08-09 12:47:31 +03:00

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;
}