mirror of https://github.com/espruino/BangleApps
443 lines
17 KiB
HTML
443 lines
17 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">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></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_2v12_banglejs2.hex" class="fw-link">click here to update to DFU 2v12</a> and then click the 'Upload' button that appears.</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">always use DFU to to the update manually</a>.</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">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>
|
|
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
|
|
</div>
|
|
|
|
|
|
|
|
<pre id="log"></pre>
|
|
|
|
<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,0x7000))", crc => {
|
|
console.log("DFU CRC = "+crc);
|
|
var version = `unknown (CRC ${crc})`;
|
|
var ok = true;
|
|
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 (!ok) {
|
|
version += `(⚠ update required)`;
|
|
}
|
|
document.getElementById("boot-version").innerHTML = version;
|
|
var versionNumber = parseFloat(version.replace(".","").replace("v","."));
|
|
if (versionNumber>=2.15)
|
|
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(function(zipFile) {
|
|
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},
|
|
]
|
|
});
|
|
}
|
|
|
|
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
|
|
document.getElementById("upload").addEventListener("click", handleUpload);
|
|
document.getElementById("info-btn").addEventListener("click", function() {
|
|
document.getElementById("info-btn").style = "display:none";
|
|
document.getElementById("info-div").style = "";
|
|
});
|
|
document.getElementById("advanced-btn").addEventListener("click", function() {
|
|
document.getElementById("advanced-btn").style = "display:none";
|
|
document.getElementById("advanced-div").style = "";
|
|
});
|
|
setTimeout(checkForFileOnServer, 10);
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|