BangleApps/apps/fwupdate/custom.html

481 lines
19 KiB
HTML

<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p>This tool allows you to update the firmware on <a href="https://www.espruino.com/Bangle.js2" target="_blank">Bangle.js 2</a> devices
from within the App Loader.</p>
<div id="fw-unknown">
<p><b>Firmware updates using the App Loader are only possible on
Bangle.js 2. For firmware updates on Bangle.js 1 please
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p>
</div>
<ul>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span>.
The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.</p>
</ul>
<div id="fw-ok" style="display:none">
<p id="fw-old-bootloader-msg">If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
will fail with a message about the DFU version. If so, please <a href="bootloader_espruino_2v25_banglejs2.hex" class="fw-link">click here to update to DFU 2v25</a> and then click the 'Upload' button that appears.</p>
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
<div id="latest-firmware" style="display:none">
<p>The currently available Espruino firmware releases are:</p>
<ul id="latest-firmware-list">
</ul>
<p>To update, click a link above and then click the 'Upload' button that appears.</p>
</div>
<p><a href="#" id="info-btn">What is DFU? ▼</a></p>
<div id="info-div" style="display:none">
<p><b>What is DFU?</b></p>
<p><b>DFU</b> stands for <b>Device Firmware Update</b>. This is the first
bit of code that runs when Bangle.js starts, and it is able to update the
Bangle.js firmware. Normally you would update firmware via this Firmware
Updater app, but if for some reason Bangle.js will not boot, you can
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">always use DFU to do the update manually</a>.
On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 at later fixes this.</p>
<p>DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p>
</div>
<p><a href="#" id="advanced-btn">Advanced ▼</a></p>
<div id="advanced-div" style="display:none">
<p><b>Advanced</b></p>
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">the Bangle.js 2 page</a>. Firmware
is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
the new firmware into internal Storage.</p>
<p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code>
file, *not* the normal <code>.hex</code> (as that contains the DFU as well).</p>
<p><b>DANGER!</b> No verification is performed on uploaded ZIP or HEX files - you could
potentially overwrite your DFU with the wrong binary and brick your Bangle.</p>
<input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br>
</div>
</div>
<pre id="log"></pre>
<p><a href="#" id="changelog-btn">Firmware ChangeLog ▼</a></p>
<div id="changelog-div" style="display:none">
<p><b>Firmware ChangeLog</b></p>
<ul>
<li><a href="https://www.espruino.com/ChangeLog" target="_blank">Released</a></li>
<li><a href="https://github.com/espruino/Espruino/blob/master/ChangeLog" target="_blank">Cutting Edge</a></li>
</ul>
</div>
<script src="../../core/lib/customize.js"></script>
<script src="../../core/lib/espruinotools.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
<script>
var hexJS; // JS to upload hex
var HEADER_LEN = 16; // size of app flash header
var APP_START = 0x26000;
var APP_MAX_LENGTH = 0xda000; // from linker file - the max size the app can be, for sanity check!
var MAX_ADDRESS = 0x1000000; // discount anything in hex file above this
var VERSION = 0x12345678; // VERSION! Use this to test firmware in JS land
var DEBUG = false;
function clearLog() {
document.getElementById('log').innerText = "";
console.log("Log Cleared");
}
function log(t) {
document.getElementById('log').innerText += t+"\n";
console.log(t);
}
function onInit(device) {
console.log("fwupdate init", device);
if (device && device.version)
document.getElementById("fw-version").innerText = device.version;
if (device && device.id=="BANGLEJS2") {
document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = "";
}
Puck.eval("[E.CRC32(E.memoryArea(0xF7000,0x6000)),E.CRC32(E.memoryArea(0xF7000,0x7000))]", crcs => {
console.log("DFU CRC (6 pages) = "+crcs[0]);
console.log("DFU CRC (7 pages) = "+crcs[1]);
var version = `unknown (CRC ${crcs[1]})`;
var ok = true;
if (crcs[0] == 1787004733) version = "2v20"; // check 6 page CRCs - the 7th page isn't used in 2v20+
else if (crcs[0] == 3816337552) version = "2v21";
else if (crcs[0] == 3329616485) version = "2v22";
else if (crcs[0] == 1569433504) version = "2v23";
else if (crcs[0] == 680675961) version = "2v24";
else if (crcs[0] == 4148062987) version = "2v25";
else { // for other versions all 7 pages are used, check those
var crc = crcs[1];
if (crc==1339551013) { version = "2v10.219"; ok = false; }
if (crc==1207580954) { version = "2v10.236"; ok = false; }
if (crc==3435933210) version = "2v11.52";
if (crc==46757280) version = "2v11.58";
if (crc==3508163280 || crc==1418074094) version = "2v12";
if (crc==4056371285) version = "2v13";
if (crc==1038322422) version = "2v14";
if (crc==2560806221) version = "2v15";
if (crc==2886730689) version = "2v16";
if (crc==156320890) version = "2v17";
if (crc==4012421318) version = "2v18";
if (crc==1856454048) version = "2v19";
}
if (!ok) {
version += `(&#9888; update required)`;
}
document.getElementById("boot-version").innerHTML = version;
var versionNumber = parseFloat(version.replace(".","").replace("v","."));
if (versionNumber>=2.25)
document.getElementById("fw-old-bootloader-msg").style.display = "none";
});
}
function checkForFileOnServer() {
function getURL(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onload = callback;
xhr.open("GET", url);
xhr.responseType = "document";
xhr.send();
}
function getFilesFromURL(url, regex, callback) {
getURL(url, function() {
//console.log(this.responseXML)
var files = [];
var elements = this.responseXML.getElementsByTagName("a");
for (var i=0;i<elements.length;i++) {
var href = elements[i].href;
if (regex.exec(href)) {
files.push(href);
}
}
callback(files);
});
}
var regex = new RegExp("_banglejs2.*zip$");
var domFirmwareList = document.getElementById("latest-firmware-list");
var domFirmware = document.getElementById("latest-firmware");
console.log("Checking server...");
getFilesFromURL("https://www.espruino.com/binaries/", regex, function(releaseFiles) {
releaseFiles.sort().reverse().forEach(function(f) {
var name = f.substr(f.substr(0,f.length-1).lastIndexOf('/')+1);
console.log("Found "+name);
domFirmwareList.innerHTML += '<li>Release: <a href="'+f+'" class="fw-link">'+name+'</a></li>';
domFirmware.style = "";
});
getFilesFromURL("https://www.espruino.com/binaries/travis/master/",regex, function(travisFiles) {
travisFiles.forEach(function(f) {
var name = f.substr(f.lastIndexOf('/')+1);
console.log("Found "+name);
domFirmwareList.innerHTML += '<li>Cutting Edge build: <a href="'+f+'" class="fw-link">'+name+'</a></li>';
domFirmware.style = "";
});
console.log("Finished check for firmware files...");
var fwlinks = document.querySelectorAll(".fw-link");
for (var i=0;i<fwlinks.length;i++)
fwlinks[i].addEventListener("click", e => {
e.preventDefault();
var href = e.target.href;
if (href) downloadURL(href).then(info=>{
document.getElementById("upload").style = ""; // show upload
});
});
});
});
}
function downloadURL(url) {
clearLog();
log("Downloading "+url);
if (url.endsWith(".zip")) {
return downloadZipFile(url);
} else if (url.endsWith(".hex")) {
return downloadHexFile(url);
} else {
log("Unknown URL "+url+" - expecting .hex or .zip extension");
return Promise.reject();
}
}
function downloadHexFile(url) {
return new Promise(resolve => {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
hexFileLoaded(this.responseText.toString());
resolve();
};
xhr.open("GET", url);
xhr.responseType = "text";
xhr.send();
});
}
function downloadZipFile(url) {
return new Promise((resolve,reject) => {
Espruino.Core.Utils.getBinaryURL(url, (err, binary) => {
if (err) return reject("Unable to download "+url);
resolve(binary);
});
}).then(convertZipFile);
}
function convertZipFile(binary) {
var info = {};
Promise.resolve(binary).then(binary => {
info.binary = binary;
return JSZip.loadAsync(binary)
}).then(async function(zipFile) {
const fileArray = Object.values(zipFile.files);
// If the contents are zipped twice, extract the contents of the inner zip file
if(fileArray.length === 1 && fileArray[0].name?.endsWith(".zip")){
const zipBlob = await zipFile.file(fileArray[0].name).async("blob");
info.zipFile = await JSZip.loadAsync(zipBlob);
}else{
info.zipFile = zipFile;
}
return info.zipFile.file("manifest.json").async("string");
}).then(function(content) {
info.manifest = JSON.parse(content).manifest;
}).then(function(content) {
console.log(info.manifest);
return info.zipFile.file(info.manifest.application.dat_file).async("arraybuffer");
}).then(function(content) {
info.dat_file = content;
}).then(function(content) {
console.log(info.manifest);
return info.zipFile.file(info.manifest.application.bin_file).async("arraybuffer");
}).then(function(content) {
info.bin_file = content;
if (info.bin_file.byteLength > APP_MAX_LENGTH) throw new Error("Firmware file is too big!");
info.storageContents = new Uint8Array(info.bin_file.byteLength + HEADER_LEN)
info.storageContents.set(new Uint8Array(info.bin_file), HEADER_LEN);
console.log("ZIP downloaded and decoded",info);
createJS_app(info.storageContents, APP_START, APP_START+info.bin_file.byteLength);
document.getElementById("upload").style = ""; // show upload
return info;
}).catch(err => log("ERROR:" + err));
}
function handleFileSelect(event) {
clearLog();
if (event.target.files.length!=1) {
log("More than one file selected!");
return;
}
var file = event.target.files[0];
var reader = new FileReader();
if (file.name.endsWith(".hex") || file.name.endsWith(".app_hex")) {
reader.onload = function(event) {
log("HEX uploaded");
document.getElementById("upload").style = ""; // show upload
hexFileLoaded(event.target.result);
};
reader.readAsText(event.target.files[0]);
} else if (file.name.endsWith(".zip")) {
reader.onload = function(event) {
log("ZIP uploaded");
convertZipFile(event.target.result);
};
reader.readAsArrayBuffer(event.target.files[0]);
} else {
log("Unknown file extension for "+file.name);
}
};
function CRC32(data) {
var crc = 0xFFFFFFFF;
data.forEach(function(d) {
crc^=d;
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
crc=(crc>>>1)^(0xEDB88320&-(crc&1));
});
return (~crc)>>>0; // >>>0 converts to unsigned 32-bit integer
}
function btoa(input) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var out = "";
var i=0;
while (i<input.length) {
var octet_a = 0|input[i++];
var octet_b = 0;
var octet_c = 0;
var padding = 0;
if (i<input.length) {
octet_b = 0|input[i++];
if (i<input.length) {
octet_c = 0|input[i++];
padding = 0;
} else
padding = 1;
} else
padding = 2;
var triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
out += b64[(triple >> 18) & 63] +
b64[(triple >> 12) & 63] +
((padding>1)?'=':b64[(triple >> 6) & 63]) +
((padding>0)?'=':b64[triple & 63]);
}
return out;
}
/* To upload the app, we write to external flash,
binary = Uint8Array of data to flash. Should include HEADER_LEN header, then bytes to flash */
function createJS_app(binary, startAddress, endAddress) {
/* typedef struct {
uint32_t address;
uint32_t size;
uint32_t CRC;
uint32_t version;
} FlashHeader; */
var bin32 = new Uint32Array(binary.buffer);
bin32[0] = startAddress;
bin32[1] = endAddress - startAddress;
bin32[2] = CRC32(new Uint8Array(binary.buffer, HEADER_LEN));
bin32[3] = VERSION; // VERSION! Use this to test ourselves
console.log("CRC 0x"+bin32[2].toString(16));
hexJS = "";//`\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${bin32[2]}) { print("FIRMWARE UP TO DATE!"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1339551013) { print("DFU 2v10.219 needs update"); load();}\n`;
hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("DFU 2v10.236 needs update"); load();}\n`;
hexJS += '\x10var s = require("Storage");\n';
hexJS += '\x10s.erase(".firmware");\n';
var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) {
var l = binary.length-i;
if (l>CHUNKSIZE) l=CHUNKSIZE;
var chunk = btoa(new Uint8Array(binary.buffer, i, l));
hexJS += `\x10s.write('.firmware', atob("${chunk}"), 0x${i.toString(16)}, ${binary.length});\n`;
}
hexJS += '\x10setTimeout(()=>E.showMessage("Rebooting..."),50);\n';
hexJS += '\x10setTimeout(()=>E.reboot(), 1000);\n';
log("Firmware update ready for upload");
}
// To upload the bootloader, we write to internal flash, right over bootloader
function createJS_bootloader(binary, startAddress, endAddress) {
var crc = CRC32(binary);
console.log("CRC 0x"+crc.toString(16));
hexJS = `\x10if (E.CRC32(E.memoryArea(${startAddress},${endAddress-startAddress}))==${crc}) { print("DFU UP TO DATE!"); load();}\n`;
hexJS += `\x10var _fw = new Uint8Array(${binary.length})\n`;
var CHUNKSIZE = 1024;
for (var i=0;i<binary.length;i+=CHUNKSIZE) {
var l = binary.length-i;
if (l>CHUNKSIZE) l=CHUNKSIZE;
var chunk = btoa(new Uint8Array(binary.buffer, binary.byteOffset+i, l));
hexJS += '\x10_fw.set(atob("'+chunk+'"), 0x'+(i).toString(16)+');\n';
}
hexJS += `\x10(function() { if (E.CRC32(_fw)!=${crc}) throw "Invalid CRC: 0x"+E.CRC32(_fw).toString(16);\n`;
hexJS += 'E.showMessage("Flashing DFU...")\n';
hexJS += 'E.setFlags({unsafeFlash:1})\n';
hexJS += 'var f = require("Flash");\n';
for (var i=startAddress;i<endAddress;i+=4096)
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += `})()\n`;
log("DFU ready for upload");
}
function hexFileLoaded(hexString) {
var hex = hexString.split("\n"); // array of lines of the hex file
function hexParseLines(dataCallback) {
var addrHi = 0;
hex.forEach(function(hexline) {
if (DEBUG) console.log(hexline);
var bytes = hexline.substr(1,2);
var addrLo = parseInt(hexline.substr(3,4),16);
var cmd = hexline.substr(7,2);
if (cmd=="02") addrHi = parseInt(hexline.substr(9,4),16) << 4; // Extended Segment Address
else if (cmd=="04") addrHi = parseInt(hexline.substr(9,4),16) << 16; // Extended Linear Address
else if (cmd=="00") {
var addr = addrHi + addrLo;
var data = [];
for (var i=0;i<16;i++) data.push(parseInt(hexline.substr(9+(i*2),2),16));
dataCallback(addr,data);
}
});
}
// Work out addresses
var startAddress, endAddress = 0;
hexParseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range
if (startAddress === undefined || addr<startAddress)
startAddress = addr;
var end = addr + data.length;
if (end > endAddress)
endAddress = end;
});
console.log(`// Data from 0x${startAddress.toString(16)} to 0x${endAddress.toString(16)} (${endAddress-startAddress} bytes)`);
// Work out data
var binary = new Uint8Array(HEADER_LEN + endAddress-startAddress);
binary.fill(0); // actually seems to assume a block is filled with 0 if not complete
hexParseLines(function(addr, data) {
if (addr>MAX_ADDRESS) return; // ignore data out of range
var binAddr = HEADER_LEN + addr - startAddress;
binary.set(data, binAddr);
if (DEBUG) console.log("i",addr.toString(16).padStart(8,0), data.map(x=>x.toString(16).padStart(2,0)).join(" "));
//console.log("o",new Uint8Array(binary.buffer, binAddr, data.length));
});
if (startAddress == 0xf7000) {
console.log("DFU - Writing to internal flash");
createJS_bootloader(new Uint8Array(binary.buffer, HEADER_LEN), startAddress, endAddress);
} else {
console.log("App - Writing to external flash");
createJS_app(binary, startAddress, endAddress);
}
}
function handleUpload() {
if (!hexJS) {
log("Hex file not loaded!");
return;
}
sendCustomizedApp({
storage:[
{name:"RAM", content:hexJS},
]
}, { noFinish: true });
}
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload);
document.getElementById("info-btn").addEventListener("click", function(e) {
document.getElementById("info-btn").style = "display:none";
document.getElementById("info-div").style = "";
e.preventDefault();
});
document.getElementById("advanced-btn").addEventListener("click", function(e) {
document.getElementById("advanced-btn").style = "display:none";
document.getElementById("advanced-div").style = "";
e.preventDefault();
});
document.getElementById("changelog-btn").addEventListener("click", function(e) {
document.getElementById("changelog-btn").style = "display:none";
document.getElementById("changelog-div").style = "";
e.preventDefault();
});
setTimeout(checkForFileOnServer, 10);
</script>
</body>
</html>