Files
RNode_Flasher/index.html
2024-07-17 00:27:56 +12:00

638 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RNode Flasher</title>
<script src="./rnode.js"></script>
<script src="./nrf52_dfu_flasher.js"></script>
<script src="./zip.min.js"></script>
<!-- tailwind css -->
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<!-- vue js -->
<script src="https://unpkg.com/vue@3"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/core.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/md5.js"></script>
</head>
<body>
<div id="app" class="space-y-2 p-3">
<div class="border bg-gray-50 p-3 rounded">
<div class="font-bold">RNode Flasher</div>
<div>Only RAK4631 is supported at this time.</div>
</div>
<div class="border bg-gray-50 p-3 rounded">
<div>1. Put device into DFU Mode</div>
<button @click="enterDfuMode" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Enter DFU Mode
</button>
</div>
<div class="border bg-gray-50 p-3 rounded">
<div>2. Select firmware.zip to flash</div>
<div class="mb-1">
<input ref="file" type="file"/>
</div>
<div v-if="!isFlashing">
<button @click="flash" :disabled="isFlashing" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Flash Now
</button>
</div>
<div v-else>
<span v-if="flashingProgress > 0">Flashing: {{flashingProgress}}%</span>
<span v-else>Flashing: please wait...</span>
<div class="mt-1 w-[200px] overflow-hidden rounded-full bg-gray-200">
<div class="h-2 rounded-full bg-blue-600" :style="{ 'width': `${flashingProgress}%`}"></div>
</div>
</div>
</div>
<div class="border bg-gray-50 p-3 rounded">
<div>3. Provision EEPROM with device info, checksum and signature.</div>
<div class="flex mb-1 space-x-1">
<div class="min-w-[70px] my-auto text-right">Product</div>
<select v-model="selectedProduct" class="min-w-[200px] bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block pl-2 pr-8">
<option :value="null" disabled>Select a Product</option>
<option v-for="product of products" :value="product">{{ product.name }}</option>
</select>
</div>
<div class="flex mb-1 space-x-1">
<div class="min-w-[70px] my-auto text-right">Model</div>
<select v-model="selectedModel" class="min-w-[200px] bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block pl-2 pr-8">
<option :value="null" disabled>Select a Model</option>
<option v-if="selectedProduct" v-for="model of selectedProduct.models" :value="model">{{ model.name }}</option>
</select>
</div>
<div>
<button @click="provision" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Provision
</button>
</div>
</div>
<div class="border bg-gray-50 p-3 rounded">
<div>4. Set Firmware Hash, for now it uses what the board knows, will fix later.</div>
<button @click="setFirmwareHash" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Set Firmware Hash
</button>
</div>
<div class="border bg-gray-50 p-3 rounded">
<div>5. Configure TNC mode: frequency, bandwidth, tx power, spreading factor, coding rate</div>
<div class="space-x-1">
<button @click="enableTncMode" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Enable
</button>
<button @click="disableTncMode" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Disable
</button>
</div>
</div>
<div class="border bg-gray-50 p-3 rounded">
<div>Extra Tools</div>
<div class="space-x-1">
<button @click="detect" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Detect RNode
</button>
<button @click="reboot" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Reboot RNode
</button>
<button @click="dumpEeprom" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Dump EEPROM
</button>
<button @click="wipeEeprom" class="border border-gray-500 px-2 bg-red-100 hover:bg-red-200 rounded">
Wipe EEPROM
</button>
</div>
<div class="text-sm text-gray-500">EEPROM dumps are shown in dev tools console.</div>
</div>
</div>
<script>
Vue.createApp({
data() {
return {
isFlashing: false,
flashingProgress: 0,
selectedProduct: null,
selectedModel: null,
products: [
{
name: "RAK4631",
id: ROM.PRODUCT_RAK4631,
models: [
{
id: ROM.MODEL_11,
name: "433 MHz",
},
{
id: ROM.MODEL_12,
name: "868 MHz",
},
{
id: ROM.MODEL_12,
name: "915 MHz",
},
{
id: ROM.MODEL_12,
name: "923 MHz",
},
],
},
],
};
},
mounted() {
},
methods: {
async askForSerialPort() {
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
}
// ask user to select device
return await navigator.serial.requestPort({
filters: [],
});
},
async enterDfuMode() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// enter dfu mode
const flasher = new Nrf52DfuFlasher(serialPort);
await flasher.enterDfuMode();
},
async flash() {
// ensure firmware file selected
const file = this.$refs["file"].files[0];
if(!file){
alert("Select a firmware file first");
return;
}
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// update progress
this.isFlashing = true;
this.flashingProgress = 0;
try {
// flash file
const flasher = new Nrf52DfuFlasher(serialPort);
await flasher.flash(file, (percentage, message) => {
this.flashingProgress = percentage;
});
// flashing successful
alert("Firmware has been flashed!");
} catch(e) {
alert("Firmware flashing failed: " + e);
console.log(e);
} finally {
this.isFlashing = false;
}
},
async detect() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
const firmwareVersion = await rnode.getFirmwareVersion();
alert("Device has RNode firmware v" + firmwareVersion);
console.log({
firmware_version: await rnode.getFirmwareVersion(),
platform: await rnode.getPlatform(),
mcu: await rnode.getMcu(),
board: await rnode.getBoard(),
device_hash: await rnode.getDeviceHash(),
firmware_hash_target: await rnode.getTargetFirmwareHash(),
firmware_hash: await rnode.getFirmwareHash(),
// rom: await rnode.getRom(),
frequency: await rnode.getFrequency(),
bandwidth: await rnode.getBandwidth(),
tx_power: await rnode.getTxPower(),
spreading_factor: await rnode.getSpreadingFactor(),
coding_rate: await rnode.getCodingRate(),
radio_state: await rnode.getRadioState(),
rx_stat: await rnode.getRxStat(),
tx_stat: await rnode.getTxStat(),
rssi_stat: await rnode.getRssiStat(),
});
await rnode.close();
},
packInt(value) {
const buffer = new ArrayBuffer(4); // 4 bytes for a 32-bit integer
const view = new DataView(buffer);
view.setUint32(0, value, false); // false for big-endian
return new Uint8Array(buffer);
},
unpackInt(byteArray) {
const buffer = new Uint8Array(byteArray).buffer; // Get the underlying ArrayBuffer from the byte array
const view = new DataView(buffer);
return view.getUint32(0, false); // false for big-endian
},
async reboot() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
// reboot
await rnode.reset();
await rnode.close();
// done
alert("Board is rebooting!");
},
async dumpEeprom() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
// get rom
const eeprom = await rnode.getRom();
if(!eeprom){
alert("Unable to retrieve eeprom!");
return;
}
// done
console.log(Utils.bytesToHex(eeprom));
await rnode.close();
},
async wipeEeprom() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// ask user to confirm
if(!confirm("Are you sure you want to wipe the eeprom on this device?")){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
// wipe eeprom
console.log("wiping eeprom");
await rnode.wipeRom();
console.log("wiping eeprom: done");
// must reboot device after wipe
await rnode.reset();
await rnode.close();
// done
alert("eeprom has been wiped!");
},
async provision() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
await rnode.close();
return;
}
const rom = await rnode.getRomAsObject();
const details = rom.parse();
if(details){
console.log(details);
alert("Eeprom is already provisioned. You must wipe it to reprovision!");
await rnode.close();
return;
}
// ensure user has selected product
if(!this.selectedProduct){
alert("Please select a product!");
await rnode.close();
return;
}
// ensure user has selected model
if(!this.selectedModel){
alert("Please select a model!");
await rnode.close();
return;
}
console.log("device is not provisioned yet, doing it now...");
// determine device info
// todo implement ui to configure these values
const product = this.selectedProduct.id;
const model = this.selectedModel.id;
const hardwareRevision = 0x1;
const serialNumber = 1;
const timestampInSeconds = Math.floor(Date.now() / 1000);
const serialBytes = this.packInt(serialNumber);
const timestampBytes = this.packInt(timestampInSeconds);
// compute device info checksum
const checksum = Utils.md5([
product,
model,
hardwareRevision,
...serialBytes,
...timestampBytes,
]);
console.log("checksum", checksum);
// write device info to eeprom
console.log("writing device info");
await rnode.writeRom(ROM.ADDR_PRODUCT, product);
console.log(Utils.bytesToHex(await rnode.getRom()));
await rnode.writeRom(ROM.ADDR_MODEL, model);
console.log(Utils.bytesToHex(await rnode.getRom()));
await rnode.writeRom(ROM.ADDR_HW_REV, hardwareRevision);
console.log(Utils.bytesToHex(await rnode.getRom()));
await rnode.writeRom(ROM.ADDR_SERIAL, serialBytes[0]);
await rnode.writeRom(ROM.ADDR_SERIAL + 1, serialBytes[1]);
await rnode.writeRom(ROM.ADDR_SERIAL + 2, serialBytes[2]);
await rnode.writeRom(ROM.ADDR_SERIAL + 3, serialBytes[3]);
console.log(Utils.bytesToHex(await rnode.getRom()));
await rnode.writeRom(ROM.ADDR_MADE, timestampBytes[0]);
await rnode.writeRom(ROM.ADDR_MADE + 1, timestampBytes[1]);
await rnode.writeRom(ROM.ADDR_MADE + 2, timestampBytes[2]);
await rnode.writeRom(ROM.ADDR_MADE + 3, timestampBytes[3]);
console.log(Utils.bytesToHex(await rnode.getRom()));
console.log("writing device info: done");
// write checksum to eeprom
console.log("writing checksum");
for(var i = 0; i < 16; i++){
await rnode.writeRom(ROM.ADDR_CHKSUM + i, checksum[i]);
}
console.log(Utils.bytesToHex(await rnode.getRom()));
console.log("writing checksum: done");
// write signature to eeprom
// fixme: actually implement signature, for now it's just zeroed out
console.log("writing signature");
for(var i = 0; i < 128; i++){
// await rnode.writeRom(ROM.ADDR_SIGNATURE + i, signature[i]);
await rnode.writeRom(ROM.ADDR_SIGNATURE + i, 0x00); // fixme: fake signature
}
console.log(Utils.bytesToHex(await rnode.getRom()));
console.log("writing signature: done");
// write info lock byte to eeprom
console.log("writing lock byte");
await rnode.writeRom(ROM.ADDR_INFO_LOCK, ROM.INFO_LOCK_BYTE);
console.log(Utils.bytesToHex(await rnode.getRom()));
console.log("writing lock byte: done");
// todo get partition hash from release.json OR directly from the firmware.bin
// partition_filename = fw_filename.replace(".zip", ".bin")
// partition_hash = get_partition_hash(rnode.platform, UPD_DIR+"/"+selected_version+"/"+partition_filename)
// todo set firmware hash in eeprom
// RNS.log("Setting firmware checksum...")
// rnode.set_firmware_hash(partition_hash)
// wait a bit for eeprom writes to complete
await Utils.sleepMillis(5000);
// done
await rnode.reset();
await rnode.close();
alert("device has been provisioned!");
},
async setFirmwareHash() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
// check if device has been provisioned
const rom = await rnode.getRomAsObject();
const details = rom.parse();
if(!details || !details.is_provisioned){
alert("Eeprom is not provisioned. You must do this first!");
await rnode.close();
return;
}
// todo: this works, but we should be calculating the firmware hash from the file, and not giving the board what it already knows
await rnode.setFirmwareHash(await rnode.getFirmwareHash());
// wait a bit for eeprom writes to complete
await Utils.sleepMillis(5000);
// reset board if it didn't do it automatically
try {
await rnode.reset();
} catch(e) {
console.log("couldn't auto reset board, probably did it automatically...");
}
// done
await rnode.close();
alert("firmware hash has been set!");
},
async enableTncMode() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
// check if device has been provisioned
const rom = await rnode.getRomAsObject();
const details = rom.parse();
if(!details || !details.is_provisioned){
alert("Eeprom is not provisioned. You must do this first!");
await rnode.close();
return;
}
// todo check if firmware hashes match, as config will not save if device has invalid target hash, because radio must be able to init
// configure
console.log("configuring");
await rnode.setFrequency(917375000);
await rnode.setBandwidth(500000);
await rnode.setTxPower(22);
await rnode.setSpreadingFactor(8);
await rnode.setCodingRate(5);
await rnode.setRadioStateOn();
console.log("configuring: done");
// save config
// fixme: for some reason, sending saveConfig ONCE doesn't write the entire config to eeprom...???
// fixme: when calling saveConfig once, it seems to miss the last 2 bytes of frequency, and doesn't set the conf ok byte...
// fixme: however, it seems to save it correctly if I send the CMD_CONF_SAVE more than once...
// fixme: note that sending the CMD_CONF_SAVE once in the python implementation seems to work fine, just not here...
console.log("saving config");
await Utils.sleepMillis(500);
await rnode.saveConfig();
await rnode.saveConfig();
console.log("saving config: done");
await Utils.sleepMillis(5000);
// done
await rnode.reset();
await rnode.close();
alert("TNC mode has been enabled!");
},
async disableTncMode() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return;
}
// check if device is an rnode
const rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await rnode.detect();
if(!isRNode){
alert("Selected device is not an RNode!");
return;
}
// check if device has been provisioned
const rom = await rnode.getRomAsObject();
const details = rom.parse();
if(!details || !details.is_provisioned){
alert("Eeprom is not provisioned. You must do this first!");
await rnode.close();
return;
}
// todo check if firmware hashes match, as config will not save if device has invalid target hash, because radio must be able to init
// configure
console.log("disabling tnc mode");
await rnode.deleteConfig();
console.log("disabling tnc mode: done");
// wait a bit for eeprom writes to complete
await Utils.sleepMillis(5000);
// done
await rnode.reset();
await rnode.close();
alert("TNC mode has been disabled!");
},
},
}).mount('#app');
</script>
</body>
</html>