Files
RNode_Flasher/index.html
2024-08-29 10:29:44 +12:00

1206 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>RNode Flasher</title>
<script src="./js/rnode.js"></script>
<script src="./js/nrf52_dfu_flasher.js"></script>
<script src="./js/zip.min.js"></script>
<!-- tailwind css -->
<script src="./js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
<!-- vue js -->
<script src="./js/vue@3.4.26/dist/vue.global.js"></script>
<script src="./js/crypto-js@3.9.1-1/core.js"></script>
<script src="./js/crypto-js@3.9.1-1/md5.js"></script>
</head>
<body class="bg-slate-300">
<div id="app" class="space-y-2 p-3">
<!-- header -->
<div class="flex border bg-gray-50 p-3 rounded shadow">
<div class="mr-3">
<img src="reticulum_logo_512.png" class="w-14 h-14"/>
</div>
<div class="my-auto">
<div class="font-bold">RNode Flasher</div>
<div class="text-sm">Developed by <a target="_blank" href="https://liamcottle.com" class="text-blue-500 hover:underline">Liam Cottle</a></div>
</div>
</div>
<div class="border bg-gray-50 rounded shadow">
<div class="border-b px-2 py-1">
1. Select your device
</div>
<div class="p-3">
<div class="flex mb-1 space-x-1">
<div class="min-w-[70px] my-auto text-right">Product</div>
<select v-model="selectedProduct" class="w-full sm:w-min sm:min-w-[250px] 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 space-x-1">
<div class="min-w-[70px] my-auto text-right">Model</div>
<select v-model="selectedModel" class="w-full sm:w-min sm:min-w-[250px] 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 to enter dfu mode on PLATFORM_NRF52 -->
<div v-if="selectedProduct?.platform === 0x70" class="p-3 border-t">
<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-t px-2 py-1">
<div class="text-sm space-x-1">
<span>Can't find your device? Open an issue on</span>
<a target="_blank" href="https://github.com/liamcottle/rnode-flasher" class="text-blue-500 hover:underline">GitHub</a>
</div>
</div>
</div>
<div class="border bg-gray-50 rounded shadow">
<div class="border-b px-2 py-1">
2. Select firmware to flash (.zip)
</div>
<div class="p-3">
<div class="mb-2">
<input ref="file" type="file"/>
</div>
<div v-if="!isFlashing" class="space-x-1">
<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-t px-2 py-1">
<div class="text-sm space-x-1">
<a target="_blank" href="https://github.com/markqvist/RNode_Firmware/releases" class="text-blue-500 hover:underline">Official Firmware</a>
<span></span>
<a target="_blank" href="https://github.com/liberatedsystems/RNode_Firmware_CE/releases" class="text-blue-500 hover:underline">CE Firmware</a>
<span></span>
<a target="_blank" href="https://github.com/attermann/microReticulum_Firmware/releases" class="text-blue-500 hover:underline">Transport Node Firmware</a>
</div>
</div>
</div>
<div class="border bg-gray-50 rounded shadow">
<div class="border-b px-2 py-1">
3. Provision EEPROM (sets device info, checksum and blank signature)
</div>
<div class="p-3">
<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 rounded shadow">
<div class="border-b px-2 py-1">
4. Set Firmware Hash (uses hash from board, will fix later)
</div>
<div class="p-3">
<button @click="setFirmwareHash" class="border border-gray-500 px-2 bg-gray-100 hover:bg-gray-200 rounded">
Set Firmware Hash
</button>
</div>
</div>
<div class="border bg-gray-50 rounded shadow">
<div class="border-b px-2 py-1">
5. Configure TNC Mode (optional)
</div>
<div class="p-3">
<div class="flex mb-1 space-x-1">
<div class="min-w-[125px] my-auto text-right">Frequency (Hz)</div>
<input v-model="configFrequency" type="number" class="w-full sm:w-min sm:min-w-[250px] bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
</div>
<div class="flex mb-1 space-x-1">
<div class="min-w-[125px] my-auto text-right">Bandwidth</div>
<select v-model="configBandwidth" class="w-full sm:w-min sm:min-w-[250px] 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 v-for="bandwidth in RNodeInterfaceDefaults.bandwidths" :value="bandwidth">{{ bandwidth / 1000 }} KHz</option>
</select>
</div>
<div class="flex mb-1 space-x-1">
<div class="min-w-[125px] my-auto text-right">Tx Power (dBm)</div>
<input v-model="configTxPower" type="number" class="w-full sm:w-min sm:min-w-[250px] bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
</div>
<div class="flex mb-1 space-x-1">
<div class="min-w-[125px] my-auto text-right">Spreading Factor</div>
<select v-model="configSpreadingFactor" class="w-full sm:w-min sm:min-w-[250px] 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 v-for="spreadingfactor in RNodeInterfaceDefaults.spreadingfactors" :value="spreadingfactor">{{ spreadingfactor }}</option>
</select>
</div>
<div class="flex mb-1 space-x-1">
<div class="min-w-[125px] my-auto text-right">Coding Rate</div>
<select v-model="configCodingRate" class="w-full sm:w-min sm:min-w-[250px] 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 v-for="codingrate in RNodeInterfaceDefaults.codingrates" :value="codingrate">{{ codingrate }}</option>
</select>
</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>
<div class="border bg-gray-50 rounded shadow">
<div class="border-b px-2 py-1">
Extra Tools
</div>
<div class="p-3">
<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>
</div>
<!-- setup web-serial-polyfill -->
<script type="module">
import { serial } from "./js/web-serial-polyfill@1.0.15/dist/serial.js";
if(!navigator.serial && navigator.usb){
navigator.serial = serial;
}
</script>
<!-- setup esptool-js -->
<script type="module">
import { ESPLoader, Transport } from "./js/esptool-js@0.4.5/bundle.js";
window.ESPLoader = ESPLoader;
window.Transport = Transport;
</script>
<script>
Vue.createApp({
data() {
return {
isFlashing: false,
flashingProgress: 0,
selectedProduct: null,
selectedModel: null,
products: [
{
name: "Heltec LoRa32 v3",
id: ROM.PRODUCT_H32_V3,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_C5,
name: "433 MHz",
},
{
id: ROM.MODEL_CA,
name: "868 MHz / 915 MHz / 923 MHz",
},
],
flash_config: {
flash_size: "8MB",
flash_files: {
"0xe000": "rnode_firmware_heltec32v3.boot_app0",
"0x0": "rnode_firmware_heltec32v3.bootloader",
"0x10000": "rnode_firmware_heltec32v3.bin",
"0x8000": "rnode_firmware_heltec32v3.partitions",
},
},
},
{
name: "RAK4631",
id: ROM.PRODUCT_RAK4631,
platform: ROM.PLATFORM_NRF52,
models: [
{
id: ROM.MODEL_11,
name: "433 MHz",
},
{
id: ROM.MODEL_12,
name: "868 MHz / 915 MHz / 923 MHz",
},
],
},
{
name: "LilyGO T-Beam",
id: ROM.PRODUCT_TBEAM,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_E4,
name: "433 MHz (with SX1278 chip)",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tbeam.boot_app0",
"0x1000": "rnode_firmware_tbeam.bootloader",
"0x10000": "rnode_firmware_tbeam.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tbeam.partitions",
},
},
},
{
id: ROM.MODEL_E9,
name: "868/915/923 MHz (with SX1276 chip)",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tbeam.boot_app0",
"0x1000": "rnode_firmware_tbeam.bootloader",
"0x10000": "rnode_firmware_tbeam.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tbeam.partitions",
},
},
},
{
id: ROM.MODEL_E3,
name: "433 MHz (with SX1268 chip)",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tbeam_sx1262.boot_app0",
"0x1000": "rnode_firmware_tbeam_sx1262.bootloader",
"0x10000": "rnode_firmware_tbeam_sx1262.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tbeam_sx1262.partitions",
},
},
},
{
id: ROM.MODEL_E8,
name: "868/915/923 MHz (with SX1262 chip)",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tbeam_sx1262.boot_app0",
"0x1000": "rnode_firmware_tbeam_sx1262.bootloader",
"0x10000": "rnode_firmware_tbeam_sx1262.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tbeam_sx1262.partitions",
},
},
},
],
},
{
name: "LilyGO LoRa32 v1.0",
id: ROM.PRODUCT_T32_10,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_BA,
name: "433 MHz",
},
{
id: ROM.MODEL_BB,
name: "868 MHz / 915 MHz / 923 MHz",
},
],
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_lora32v10.boot_app0",
"0x1000": "rnode_firmware_lora32v10.bootloader",
"0x10000": "rnode_firmware_lora32v10.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_lora32v10.partitions",
},
},
},
{
name: "LilyGO LoRa32 v2.0",
id: ROM.PRODUCT_T32_20,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_B3,
name: "433 MHz",
},
{
id: ROM.MODEL_B8,
name: "868 MHz / 915 MHz / 923 MHz",
},
],
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_lora32v20.boot_app0",
"0x1000": "rnode_firmware_lora32v20.bootloader",
"0x10000": "rnode_firmware_lora32v20.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_lora32v20.partitions",
},
},
},
{
name: "LilyGO LoRa32 v2.1",
id: ROM.PRODUCT_T32_21,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_B4,
name: "433 MHz",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_lora32v21.boot_app0",
"0x1000": "rnode_firmware_lora32v21.bootloader",
"0x10000": "rnode_firmware_lora32v21.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_lora32v21.partitions",
},
},
},
{
id: ROM.MODEL_B9,
name: "868/915/923 MHz",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_lora32v21.boot_app0",
"0x1000": "rnode_firmware_lora32v21.bootloader",
"0x10000": "rnode_firmware_lora32v21.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_lora32v21.partitions",
},
},
},
// The TCXO model codes are only used here to select the correct firmware,
// actual model codes in firmware is still 0xB4 and 0xB9.
// https://github.com/markqvist/Reticulum/blob/master/RNS/Utilities/rnodeconf.py#L156
// https://github.com/markqvist/Reticulum/blob/master/RNS/Utilities/rnodeconf.py#L3283
{
id: ROM.MODEL_B4_TCXO,
mapped_id: ROM.MODEL_B4, // this device uses the same model code, but different firmware file
name: "433 MHz, with TCXO",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_lora32v21_tcxo.boot_app0",
"0x1000": "rnode_firmware_lora32v21_tcxo.bootloader",
"0x10000": "rnode_firmware_lora32v21_tcxo.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_lora32v21_tcxo.partitions",
},
},
},
{
id: ROM.MODEL_B9_TCXO,
mapped_id: ROM.MODEL_B9, // this device uses the same model code, but different firmware file
name: "868/915/923 MHz, with TCXO",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_lora32v21_tcxo.boot_app0",
"0x1000": "rnode_firmware_lora32v21_tcxo.bootloader",
"0x10000": "rnode_firmware_lora32v21_tcxo.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_lora32v21_tcxo.partitions",
},
},
},
],
},
],
// Liam's default config for New Zealand / LongFast on Slot 10
configFrequency: 917375000,
configBandwidth: 250000,
configTxPower: 22,
configSpreadingFactor: 11,
configCodingRate: 5,
RNodeInterfaceDefaults: {
bandwidths: [ // bandwidth in hz
7800, // 7.8 kHz
10400, // 10.4 kHz
15600, // 15.6 kHz
20800, // 20.8 kHz
31250, // 31.25 kHz
41700, // 41.7 kHz
62500, // 62.5 kHz
125000, // 125 kHz
250000, // 250 kHz
500000, // 500 kHz
],
codingrates: [
5, // 4:5
6, // 4:6
7, // 4:7
8, // 4:8
],
spreadingfactors: [
7,
8,
9,
10,
11,
12,
],
},
};
},
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
console.log("entering dfu mode");
const flasher = new Nrf52DfuFlasher(serialPort);
await flasher.enterDfuMode();
console.log("entering dfu mode: done");
alert("Device should now be in DFU mode.");
},
async flash() {
switch(this.selectedProduct?.platform){
case ROM.PLATFORM_ESP32: {
await this.flashEsp32();
break;
}
case ROM.PLATFORM_NRF52: {
await this.flashNrf52();
break;
}
default: {
alert("Please select a device first");
break;
}
}
},
async flashNrf52() {
// 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;
}
// close port
console.log("Closing serial port");
await this.serialPort.close();
},
async flashEsp32() {
// ensure ESPLoader is available
if(!window.ESPLoader){
alert("esptool-js could not be loaded.");
return;
}
// ensure flash config is known, use from selected model, or fallback to selected product
const flashConfig = this.selectedModel?.flash_config ?? this.selectedProduct?.flash_config;
if(!flashConfig){
alert("Flash config is not available for the selected device.");
return;
}
// 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 {
// read zip file
const blobReader = new window.zip.BlobReader(file);
const zipReader = new window.zip.ZipReader(blobReader);
const zipEntries = await zipReader.getEntries();
// get files to flash
const filesToFlash = [];
for(const [address, filename] of Object.entries(flashConfig.flash_files)){
// find file inside zip
const file = zipEntries.find((zipEntry) => zipEntry.filename === filename);
if(!file){
throw filename + " not found in firmware file!";
}
// get file data as binary string
const blob = await file.getData(new window.zip.BlobWriter());
const data = await this.readAsBinaryString(blob); // fixme: deprecated, but works for now
// add to files to flash
filesToFlash.push({
"address": parseInt(address),
"data": data,
});
}
// init transport and esploader
const transport = new window.Transport(serialPort, true);
const esploader = new window.ESPLoader({
transport: transport,
baudrate: 921600,
debugLogging: false,
enableTracing: false,
terminal: {
clean() {
},
writeLine(data) {
console.log(data);
},
write(data) {
console.log(data);
},
},
});
// load device info
await esploader.main();
// flash device
await esploader.writeFlash({
fileArray: filesToFlash,
flashSize: flashConfig.flash_size,
flashMode: "DIO",
flashFreq: "80MHz",
eraseAll: false,
compress: true,
calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image)),
reportProgress: (fileIndex, written, total) => {
const currentFilePercentage = (written / total) * 100;
this.flashingProgress = Math.floor(currentFilePercentage);
},
});
// reboot device
await transport.setDTR(false);
await new Promise((resolve) => setTimeout(resolve, 100));
await transport.setDTR(true);
// flashing successful
alert("Firmware has been flashed!");
} catch(e) {
alert("Firmware flashing failed: " + e);
console.log(e);
} finally {
this.isFlashing = false;
}
// close port
console.log("Closing serial port");
await serialPort.close();
},
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;
// use mapped_id if available, else fallback to id, some devices use the same model, but different firmware file
const model = this.selectedModel.mapped_id ?? 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)
// RNS.log("Generating a new device signing key...")
// device_signer = RNS.Identity()
// device_signer.to_file(FWD_DIR+"/device.key")
// RNS.log("Device signing key written to "+str(FWD_DIR+"/device.key"))
// if not os.path.isfile(FWD_DIR+"/signing.key"):
// RNS.log("Generating a new EEPROM signing key...")
// private_key = rsa.generate_private_key(
// public_exponent=65537,
// key_size=1024,
// backend=default_backend()
// )
// private_bytes = private_key.private_bytes(
// encoding=serialization.Encoding.DER,
// format=serialization.PrivateFormat.PKCS8,
// encryption_algorithm=serialization.NoEncryption()
// )
// public_key = private_key.public_key()
// public_bytes = public_key.public_bytes(
// encoding=serialization.Encoding.DER,
// format=serialization.PublicFormat.SubjectPublicKeyInfo
// )
// can get partition hash from releases.json
// partition_hash = bytes.fromhex(release_info.split()[1])
// await rnode.indicateFirmwareUpdate();
// await rnode.setFirmwareHash(partition_hash);
// todo get signing.key
// file = open(key_path, "rb")
// private_bytes = file.read()
// file.close()
// private_key = serialization.load_der_private_key(
// private_bytes,
// password=None,
// backend=default_backend()
// )
// public_key = private_key.public_key()
// public_bytes = public_key.public_bytes(
// encoding=serialization.Encoding.DER,
// format=serialization.PublicFormat.SubjectPublicKeyInfo
// )
// signature = private_key.sign(
// checksum,
// padding.PSS(
// mgf=padding.MGF1(hashes.SHA256()),
// salt_length=padding.PSS.MAX_LENGTH
// ),
// hashes.SHA256()
// )
// 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
console.log("setting firmware hash");
await rnode.setFirmwareHash(await rnode.getFirmwareHash());
console.log("setting firmware hash: done");
// 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(this.configFrequency);
await rnode.setBandwidth(this.configBandwidth);
await rnode.setTxPower(this.configTxPower);
await rnode.setSpreadingFactor(this.configSpreadingFactor);
await rnode.setCodingRate(this.configCodingRate);
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!");
},
async readAsBinaryString(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsBinaryString(blob);
});
},
},
watch: {
selectedProduct() {
// reset selected model when changing selected product
this.selectedModel = null;
},
},
}).mount('#app');
</script>
</body>
</html>