585 lines
22 KiB
HTML
585 lines
22 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>
|
|
<button @click="provision" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
|
|
Provision
|
|
</button>
|
|
</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,
|
|
};
|
|
},
|
|
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!");
|
|
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;
|
|
}
|
|
|
|
console.log("device is not provisioned yet, doing it now...");
|
|
|
|
// determine device info
|
|
// todo implement ui to configure these values
|
|
const product = ROM.PRODUCT_RAK4631;
|
|
const model = ROM.MODEL_12;
|
|
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;
|
|
}
|
|
|
|
// log old hashes
|
|
console.log({
|
|
old_firmware_hash: await rnode.getFirmwareHash(),
|
|
old_target_firmware_hash: await rnode.getTargetFirmwareHash(),
|
|
});
|
|
|
|
// 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);
|
|
|
|
// log new hashes
|
|
console.log({
|
|
new_firmware_hash: await rnode.getFirmwareHash(),
|
|
new_target_firmware_hash: await rnode.getTargetFirmwareHash(),
|
|
});
|
|
|
|
// done
|
|
await rnode.reset();
|
|
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> |