Files
RNode_Flasher/index.html

2975 lines
139 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 (DIY)</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.prod.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-[#1e293b]">
<div id="app" class="space-y-2 p-3">
<!-- header -->
<div class="flex border bg-[#1e293b] 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 text-white">RNode Flasher</div>
<div class="text-sm text-gray-300">Supports factory and DIY devices. Originally by <a target="_blank" href="https://liamcottle.com" class="text-blue-500 hover:underline">Liam Cottle</a></div>
<div class="text-sm text-gray-300">Maintained by <a target="_blank" href="https://t.me/TheLT#" class="text-blue-500 hover:underline">Nickie Deuxyeux</a></div>
<div class="text-sm text-gray-300">Firmware archive (binaries) available <a target="_blank" href="/firmware/" class="text-blue-500 hover:underline">here</a></div>
<div class="text-sm text-gray-300">Firmware sources available <a target="_blank" href="https://git.rns.moscow/explore/repos/" class="text-blue-500 hover:underline">here</a></div>
</div>
</div>
<div class="flex border bg-[#1e293b] p-3 rounded shadow">
<div class="my-auto">
<div class="font-bold text-white">Current version: 1.86</div>
<div class="text-sm text-gray-300">Updated on: 2026-Apr-26</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
1. Select your device
</div>
<div class="p-3">
<div class="mb-2">
<label class="block text-gray-300 mb-1">Product</label>
<select v-model="selectedProduct" class="w-full bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<option :value="null" disabled>Select a Product</option>
<option v-for="product of products" :value="product">{{ product.name }}</option>
</select>
</div>
<div v-if="selectedProduct" class="mb-2">
<label class="block text-gray-300 mb-1">Model</label>
<select v-model="selectedModel" class="w-full bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<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>
<!-- button to enter dfu mode on PLATFORM_NRF52 -->
<div v-if="selectedProduct?.platform === 0x70" class="p-3 border-t space-x-1">
<button @click="enterDfuMode" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Enter DFU Mode
</button>
<button @click="downloadEraseUF2" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Download Erase UF2
</button>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
2. Select firmware to flash (.zip)
</div>
<div class="p-3">
<div class="mb-2">
<input ref="file" type="file" class="bg-gray-700 text-white"/>
</div>
<div v-if="!isFlashing" class="space-x-1">
<button @click="flash" :disabled="isFlashing" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Flash Now
</button>
<button @click="eraseEsp32" :disabled="isFlashing" class="border border-red-500 px-2 bg-red-700 hover:bg-red-600 rounded text-white ml-2">
Erase Flash
</button>
</div>
<div v-else>
<span v-if="flashingProgress > 0" class="text-white">Flashing: {{flashingProgress}}%</span>
<span v-else class="text-white">Flashing: please wait...</span>
<div class="mt-1 w-[200px] overflow-hidden rounded-full bg-gray-700">
<div class="h-2 rounded-full bg-blue-600" :style="{ 'width': `${flashingProgress}%`}"></div>
</div>
</div>
</div>
<div class="border-t px-2 py-1 text-sm text-white">
<div v-if="selectedProduct && selectedModel && recommendedFirmwareFilename">
<span>Classic Firmware Download</span>
<span v-if="selectedProduct && selectedModel && recommendedFirmwareFilename">: <a target="_blank" :href="`firmware/classic/latest/${recommendedFirmwareFilename}`" class="text-blue-500 hover:underline">{{ recommendedFirmwareFilename }}</a>&nbsp;</span>
</div>
<div v-if="selectedProduct && selectedModel && FirmwareRepoClassic">
<span>Classic Firmware Information</span>
<span>: <a target="_blank" :href="`${FirmwareRepoClassic}`" class="text-blue-500 hover:underline">{{ FirmwareRepoClassic }}</a></span>
</div>
<div v-if="selectedProduct && selectedModel && recommendedFirmwareFilenameMicroReticulum">
<span>MicroReticulum Firmware Download</span>
<span v-if="selectedProduct && selectedModel && recommendedFirmwareFilenameMicroReticulum">: <a target="_blank" :href="`firmware/microreticulum/latest/${recommendedFirmwareFilenameMicroReticulum}`" class="text-blue-500 hover:underline">{{ recommendedFirmwareFilenameMicroReticulum }}</a>&nbsp;</span>
</div>
<div v-if="selectedProduct && selectedModel && FirmwareRepoMicroReticulum">
<span>MicroReticulum Firmware Information</span>
<span>: <a target="_blank" :href="`${FirmwareRepoMicroReticulum}`" class="text-blue-500 hover:underline">{{ FirmwareRepoMicroReticulum }}</a></span>
</div>
<div v-if="selectedProduct && selectedModel && recommendedFirmwareFilenameCustom">
<span>Custom Firmware Download</span>
<span v-if="selectedProduct && selectedModel && recommendedFirmwareFilenameCustom">: <a target="_blank" :href="`firmware/${recommendedFirmwareFilenameCustom}`" class="text-blue-500 hover:underline">{{ recommendedFirmwareFilenameCustom }}</a>&nbsp;</span>
</div>
<div v-if="selectedProduct && selectedModel && FirmwareRepoCustom">
<span>Custom Firmware Information</span>
<span>: <a target="_blank" :href="`${FirmwareRepoCustom}`" class="text-blue-500 hover:underline">{{ FirmwareRepoCustom }}</a></span>
</div>
<div v-if="selectedProduct && selectedModel && recommendedFirmwareFilenameRTNode">
<span>RTNode Firmware Download</span>
<span v-if="selectedProduct && selectedModel && recommendedFirmwareFilenameRTNode">: <a target="_blank" :href="`firmware/rtnode/latest/${recommendedFirmwareFilenameRTNode}`" class="text-blue-500 hover:underline">{{ recommendedFirmwareFilenameRTNode }}</a>&nbsp;</span>
</div>
<div v-if="selectedProduct && selectedModel && FirmwareRepoRTNode">
<span>RTNode Firmware Information</span>
<span>: <a target="_blank" :href="`${FirmwareRepoRTNode}`" class="text-blue-500 hover:underline">{{ FirmwareRepoRTNode }}</a></span>
</div>
<div v-if="selectedProduct && selectedModel && HardwareInfo">
<span>Hardware Information</span>
<span>: <a target="_blank" :href="`${HardwareInfo}`" class="text-blue-500 hover:underline">{{ HardwareInfo }}</a></span>
</div>
</div>
<div class="border-t px-2 py-1 text-sm text-white">
<div>Common error messages</div>
<div>• Missing Config: You need to provision the EEPROM in step 3.</div>
<div>• Firmware Corrupt: You need to set the firmware hash in step 4.</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
3. Provision EEPROM (sets device info, checksum and blank signature)
</div>
<div class="p-3">
<button v-if="!isProvisioning" @click="provision" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Provision
</button>
<div v-else class="flex items-center space-x-1">
<div class="mr-1">
<svg class="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<div class="text-white">Provisioning: please wait...</div>
</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
4. Set Firmware Hash (uses hash from board, will fix later)
</div>
<div class="p-3">
<button v-if="!isSettingFirmwareHash" @click="setFirmwareHash" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Set Firmware Hash
</button>
<div v-else class="flex items-center space-x-1">
<div class="mr-1">
<svg class="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<div class="text-white">Setting Firmware Hash: please wait...</div>
</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
5. Done
</div>
<div class="px-2 py-1 text-sm text-white">
<div>• If you made it this far, and all previous steps were successful, your RNode should be ready to use.</div>
<div>• To use RNode with <a target="_blank" href="https://github.com/liamcottle/reticulum-meshchat" class="text-blue-500 hover:underline">MeshChat</a>, you will need to add an <u>RNodeInterface</u> in the <u>Interfaces → Add Interface</u> page.</div>
<div>• To use RNode with <a target="_blank" href="https://github.com/markqvist/Sideband/" class="text-blue-500 hover:underline">Sideband</a>, you will need to configure it in <u>Hardware → RNode</u> and enable <u>Connectivity → Connect via RNode</u>.</div>
<div>• You must restart MeshChat and Sideband for interface setting changes to take effect, otherwise nothing will happen!</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
Advanced Tools
</div>
<div class="p-3">
<div class="space-x-1">
<button @click="detect" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Detect RNode
</button>
<button @click="reboot" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Reboot RNode
</button>
<button @click="readDisplay" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Read Display
</button>
<button @click="dumpEeprom" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Dump EEPROM
</button>
<button @click="wipeEeprom" class="border border-gray-500 px-2 bg-red-900 hover:bg-red-800 rounded text-white">
Wipe EEPROM
</button>
</div>
<!-- show rnode display if available -->
<div v-if="rnodeDisplayImage">
<img :src="rnodeDisplayImage" class="h-28"/>
</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
Configure Bluetooth (optional)
</div>
<div class="border-b px-2 py-1 text-sm text-gray-300">
<div>• Bluetooth is not supported on all devices.</div>
<div>• Some devices use Bluetooth Classic, and some use BLE (Bluetooth Low Energy)</div>
<div>• Put the RNode into Bluetooth Pairing mode, then connect to it from Android Bluetooth settings.</div>
<div>• Once you have initiated a pair request from Android, a PIN should show on the RNode display.</div>
<div>• In Sideband you will need to enable <u>Connect using Bluetooth</u> in <u>Hardware → RNode</u>.</div>
<div>• If your device uses BLE you will also need to enable <u>Device requires BLE</u> in <u>Hardware → RNode</u>.</div>
<div>• Don't forget to restart Sideband for the setting changes to take effect, otherwise nothing will happen!</div>
</div>
<div class="p-3">
<div class="space-x-1">
<button @click="enableBluetooth" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Enable
</button>
<button @click="disableBluetooth" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Disable
</button>
<button @click="startBluetoothPairing" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Start Pairing
</button>
</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
Configure TNC Mode / Transport Mode
</div>
<div class="border-b px-2 py-1 text-sm text-gray-300">
<div>• TNC mode allows an RNode to be used as a KISS compatible TNC over the Serial Port.</div>
<div>• This mode makes it usable with amateur radio software that can talk to a KISS TNC over a serial port.</div>
<div>• You must leave TNC mode disabled when using RNode with apps like MeshChat or Sideband.</div>
</div>
<div class="p-3">
<div class="flex mb-1 space-x-1">
<div class="min-w-[125px] my-auto text-right text-white">Frequency (Hz)</div>
<input v-model="configFrequency" type="number" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 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 text-white">Bandwidth</div>
<select v-model="configBandwidth" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 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 text-white">Tx Power (dBm)</div>
<input v-model="configTxPower" type="number" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 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 text-white">Spreading Factor</div>
<select v-model="configSpreadingFactor" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 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 text-white">Coding Rate</div>
<select v-model="configCodingRate" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 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-700 hover:bg-gray-600 rounded text-white">
Enable
</button>
<button @click="disableTncMode" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Disable
</button>
</div>
</div>
</div>
<div class="border-b px-2 py-1 text-white">
Configure Display (optional)
</div>
<div class="p-3 space-y-2">
<div class="flex space-x-1">
<div class="my-auto text-white">Rotation</div>
<button @click="setDisplayRotation(0)" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
0
</button>
<button @click="setDisplayRotation(1)" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
1
</button>
<button @click="setDisplayRotation(2)" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
2
</button>
<button @click="setDisplayRotation(3)" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
3
</button>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="my-auto text-right text-gray-300">Intensity</div>
<input v-model.number="displayIntensity" type="number" min="0" max="255" class="w-full sm:w-min bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<button @click="setDisplayIntensity(displayIntensity)" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Set Display Intensity
</button>
</div>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="my-auto text-right text-gray-300">Timeout (sec)</div>
<input v-model.number="displayTimeout" type="number" min="0" max="255" class="w-full sm:w-min bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<button @click="setDisplayTimeout(displayTimeout)" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Set Display Timeout
</button>
</div>
</div>
<div class="flex space-x-1">
<div class="my-auto text-white">Reconditioning</div>
<button @click="startDisplayReconditioning" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Start
</button>
<button @click="reboot" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Stop
</button>
</div>
</div>
</div>
<div class="border bg-[#1e293b] rounded shadow">
<div class="border-b px-2 py-1 text-white">
Configure WiFi (optional)
</div>
<div class="border-b px-2 py-1 text-sm text-gray-300">
<div>• WiFi configuration is supported only on ESP32 devices.</div>
<div>• Simultaneous operation of Bluetooth and WiFi is unsupported.</div>
<div>• For static IP configuration both the address and the netmask need to be set.</div>
<div>• Unset both IP and Netmask to use DHCP.</div>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="min-w-[125px] my-auto text-right text-gray-300">Mode</div>
<label class="text-white">
<input type="radio" v-model="wifiMode" :value="0" class="mr-1 bg-gray-500 border-gray-100 focus:ring-blue-100">
OFF
</label>
<label class="text-white">
<input type="radio" v-model="wifiMode" :value="1" class="mr-1 bg-gray-500 border-gray-100 focus:ring-blue-100">
STATION
</label>
<label class="text-white">
<input type="radio" v-model="wifiMode" :value="2" class="mr-1 bg-gray-500 border-gray-100 focus:ring-blue-100">
AP
</label>
<div class="space-x-1 flex items-center">
<button @click="setWiFiMode" :disabled="wifiMode === null" class="border px-2 rounded text-white" :class="wifiMode === null ? 'border-gray-600 bg-gray-800 text-gray-600 cursor-not-allowed' : 'border-gray-500 bg-gray-700 hover:bg-gray-600'">
Set Mode
</button>
</div>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="min-w-[125px] my-auto text-right text-gray-300">Channel</div>
<input v-model.number="wifiChannel" type="number" min="1" max="14" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<button @click="setWiFiChannel" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Set Channel
</button>
</div>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="min-w-[125px] my-auto text-right text-gray-300">SSID</div>
<input v-model="wifiSSID" type="text" placeholder="SSID" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<button @click="setWiFiSSID" :disabled="!isSsidValid" :class="isSsidValid ? 'border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white' : 'border border-gray-600 px-2 bg-gray-800 text-gray-500 cursor-not-allowed rounded'">
Set SSID
</button>
<button @click="unsetWiFiSSID" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Unset SSID
</button>
</div>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="min-w-[125px] my-auto text-right text-gray-300">PSK (Password)</div>
<input v-model="wifiPSK" type="password" placeholder="PSK (8 characters minimum)" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<!-- Tooltip wrapper -->
<div class="relative inline-block group">
<button
@click="setWiFiPSK"
:disabled="!isPskValid"
:class="isPskValid
? 'border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white'
: 'border border-gray-600 px-2 bg-gray-800 text-gray-500 cursor-not-allowed rounded'"
>
Set PSK
</button>
<!-- Tooltip -->
<div
v-if="!isPskValid"
class="absolute left-1/2 -translate-x-1/2 -top-8
hidden group-hover:block
bg-gray-900 text-xs text-red-400
px-2 py-1 rounded border border-gray-600
whitespace-nowrap"
>
Password must be at least 8 characters.
</div>
</div>
<button
@click="unsetWiFiPSK"
class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white"
>
Unset PSK
</button>
</div>
</div>
<div class="flex mb-1 space-x-1 mt-2">
<div class="min-w-[125px] my-auto text-right text-gray-300">IP Address</div>
<input v-model="wifiIP" type="text" placeholder="192.168.1.100" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<button @click="setWiFiIP" :disabled="!isIpValid" :class="isIpValid ? 'border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white' : 'border border-gray-600 px-2 bg-gray-800 text-gray-500 cursor-not-allowed rounded'">
Set IP
</button>
<button @click="unsetWiFiIP" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Unset IP
</button>
</div>
</div>
<div class="flex mb-1 space-x-1 mt-2 items-center">
<div class="min-w-[125px] my-auto text-right text-gray-300">Netmask</div>
<input v-model="wifiNM" type="text" placeholder="255.255.255.0" class="w-full sm:w-min sm:min-w-[250px] bg-gray-700 border border-gray-600 text-gray-200 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block px-2">
<div class="space-x-1 flex items-center">
<button @click="setWiFiNM" :disabled="!isNetmaskValid" :class="isNetmaskValid ? 'border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white' : 'border border-gray-600 px-2 bg-gray-800 text-gray-500 cursor-not-allowed rounded'">
Set Netmask
</button>
<button @click="unsetWiFiNM" class="border border-gray-500 px-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
Unset Netmask
</button>
</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.5.6/bundle.js";
window.ESPLoader = ESPLoader;
window.Transport = Transport;
</script>
<script>
Vue.createApp({
data() {
return {
rnode: null,
isFlashing: false,
flashingProgress: 0,
isProvisioning: false,
isSettingFirmwareHash: false,
rnodeDisplayImage: null,
selectedProduct: null,
selectedModel: null,
products: [
{
name: "Heltec LoRa32 v2",
id: ROM.PRODUCT_H32_V2,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_C4,
name: "433 MHz",
},
{
id: ROM.MODEL_C9,
name: "868 MHz / 915 MHz / 923 MHz",
},
],
firmware_filename_classic: "rnode_firmware_heltec32v2.zip",
firmware_filename_microreticulum: "rnode_firmware_heltec32v2.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://heltec.org/project/wifi-lora-32v2/",
flash_config: {
flash_size: "8MB",
flash_files: {
"0xe000": "rnode_firmware_heltec32v2.boot_app0",
"0x1000": "rnode_firmware_heltec32v2.bootloader",
"0x10000": "rnode_firmware_heltec32v2.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_heltec32v2.partitions",
},
},
},
{
name: "Heltec LoRa32 v3",
id: ROM.PRODUCT_H32_V3,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_C5,
name: "433 MHz",
firmware_filename_classic: "rnode_firmware_heltec32v3.zip",
firmware_filename_microreticulum: "rnode_firmware_heltec32v3.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://heltec.org/project/wifi-lora-32-v3/",
flash_config: {
flash_size: "8MB",
flash_files: {
"0xe000": "rnode_firmware_heltec32v3.boot_app0",
"0x0": "rnode_firmware_heltec32v3.bootloader",
"0x10000": "rnode_firmware_heltec32v3.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_heltec32v3.partitions",
},
},
},
{
id: ROM.MODEL_CA,
name: "868 MHz / 915 MHz / 923 MHz",
firmware_filename_classic: "rnode_firmware_heltec32v3.zip",
firmware_filename_microreticulum: "rnode_firmware_heltec32v3.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://heltec.org/project/wifi-lora-32-v3/",
flash_config: {
flash_size: "8MB",
flash_files: {
"0xe000": "rnode_firmware_heltec32v3.boot_app0",
"0x0": "rnode_firmware_heltec32v3.bootloader",
"0x10000": "rnode_firmware_heltec32v3.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_heltec32v3.partitions",
},
},
},
{
id: ROM.MODEL_CA,
name: "RTNode Firmware",
firmware_filename_rtnode: "rtnode_heltec_v3.zip",
firmware_repo_rtnode: "https://git.rns.moscow/deuxyeux/RTNode-HeltecV4",
hardware_info: "https://heltec.org/project/wifi-lora-32-v3/",
flash_config: {
flash_size: "16MB",
flash_files: {
"0xe000": "boot_app0.bin",
"0x0": "bootloader.bin",
"0x10000": "firmware.bin",
"0x8000": "partitions.bin",
},
},
},
],
},
{
name: "Heltec LoRa32 v4",
id: ROM.PRODUCT_H32_V4,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_C8,
name: "868 MHz / 915 MHz / 923 MHz with PA (V4.2-V4.3)",
firmware_filename_classic: "rnode_firmware_heltec32v4pa.zip",
firmware_filename_microreticulum: "rnode_firmware_heltec32v4pa.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://heltec.org/project/wifi-lora-32-v4/",
flash_config: {
flash_size: "16MB",
flash_files: {
"0xe000": "rnode_firmware_heltec32v4pa.boot_app0",
"0x0": "rnode_firmware_heltec32v4pa.bootloader",
"0x10000": "rnode_firmware_heltec32v4pa.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_heltec32v4pa.partitions",
},
},
},
{
id: ROM.MODEL_C8,
name: "RTNode Firmware",
firmware_filename_rtnode: "rtnode_heltec_v4.zip",
firmware_repo_rtnode: "https://git.rns.moscow/deuxyeux/RTNode-HeltecV4",
hardware_info: "https://heltec.org/project/wifi-lora-32-v4/",
flash_config: {
flash_size: "16MB",
flash_files: {
"0xe000": "boot_app0.bin",
"0x0": "bootloader.bin",
"0x10000": "firmware.bin",
"0x8000": "partitions.bin",
},
},
},
],
},
{
name: "Heltec T114",
id: ROM.PRODUCT_HELTEC_T114,
platform: ROM.PLATFORM_NRF52,
models: [
{
id: ROM.MODEL_C6,
name: "470-510 MHz (HT-n5262-LF)",
},
{
id: ROM.MODEL_C7,
name: "863-928 MHz (HT-n5262-HF)",
},
],
firmware_filename_classic: "rnode_firmware_heltec_t114.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://heltec.org/project/mesh-node-t114/",
},
{
name: "SenseCAP T1000E",
id: ROM.PRODUCT_SENSECAP_T1000E,
platform: ROM.PLATFORM_NRF52,
models: [
{
id: ROM.MODEL_25,
name: "863-928 MHz",
},
],
firmware_filename_classic: "rnode_firmware_t1000e.zip",
firmware_repo_classic: "https://github.com/cobraPA",
hardware_info: "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html",
},
{
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",
},
],
firmware_filename_classic: "rnode_firmware_lora32v10.zip",
firmware_filename_microreticulum: "rnode_firmware_lora32v10.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/lora-v1-0-kit",
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",
},
],
firmware_filename_classic: "rnode_firmware_lora32v20.zip",
firmware_filename_microreticulum: "rnode_firmware_lora32v20.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/lora-v1-3",
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",
firmware_filename_classic: "rnode_firmware_lora32v21.zip",
firmware_filename_microreticulum: "rnode_firmware_lora32v21.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/lora3",
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",
firmware_filename_classic: "rnode_firmware_lora32v21.zip",
firmware_filename_microreticulum: "rnode_firmware_lora32v21.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/lora3",
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",
firmware_filename_classic: "rnode_firmware_lora32v21_tcxo.zip",
firmware_filename_microreticulum: "rnode_firmware_lora32v21.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/lora3",
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",
firmware_filename_classic: "rnode_firmware_lora32v21_tcxo.zip",
firmware_filename_microreticulum: "rnode_firmware_lora32v21.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/lora3",
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",
},
},
},
],
},
{
name: "LilyGO LoRa T3S3",
id: ROM.PRODUCT_RNODE,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_A5,
name: "433 MHz (with SX1278 chip)",
firmware_filename_classic: "rnode_firmware_t3s3_sx127x.zip",
firmware_filename_microreticulum: "rnode_firmware_t3s3_sx127x.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t3s3-v1-0",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_t3s3_sx127x.boot_app0",
"0x0": "rnode_firmware_t3s3_sx127x.bootloader",
"0x10000": "rnode_firmware_t3s3_sx127x.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_t3s3_sx127x.partitions",
},
},
},
{
id: ROM.MODEL_AA,
name: "868/915/923 MHz (with SX1276 chip)",
firmware_filename_classic: "rnode_firmware_t3s3_sx127x.zip",
firmware_filename_microreticulum: "rnode_firmware_t3s3_sx127x.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t3s3-v1-0",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_t3s3_sx127x.boot_app0",
"0x0": "rnode_firmware_t3s3_sx127x.bootloader",
"0x10000": "rnode_firmware_t3s3_sx127x.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_t3s3_sx127x.partitions",
},
},
},
{
id: ROM.MODEL_A1,
name: "433 MHz (with SX1268 chip)",
firmware_filename_classic: "rnode_firmware_t3s3.zip",
firmware_filename_microreticulum: "rnode_firmware_t3s3.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t3s3-v1-0",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_t3s3.boot_app0",
"0x0": "rnode_firmware_t3s3.bootloader",
"0x10000": "rnode_firmware_t3s3.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_t3s3.partitions",
},
},
},
{
id: ROM.MODEL_A6,
name: "868/915/923 MHz (with SX1262 chip)",
firmware_filename_classic: "rnode_firmware_t3s3.zip",
firmware_filename_microreticulum: "rnode_firmware_t3s3.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t3s3-v1-0",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_t3s3.boot_app0",
"0x0": "rnode_firmware_t3s3.bootloader",
"0x10000": "rnode_firmware_t3s3.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_t3s3.partitions",
},
},
},
{
id: ROM.MODEL_AC,
name: "2.4 GHz (with SX1280 chip)",
firmware_filename_classic: "rnode_firmware_t3s3_sx1280_pa.zip",
firmware_filename_microreticulum: "rnode_firmware_t3s3_sx1280_pa.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t3s3-v1-0",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_t3s3_sx1280_pa.boot_app0",
"0x0": "rnode_firmware_t3s3_sx1280_pa.bootloader",
"0x10000": "rnode_firmware_t3s3_sx1280_pa.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_t3s3_sx1280_pa.partitions",
},
},
},
],
},
{
name: "LilyGO T-Beam",
id: ROM.PRODUCT_TBEAM,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_E4,
name: "433 MHz (with SX1278 chip)",
firmware_filename_classic: "rnode_firmware_tbeam.zip",
firmware_filename_microreticulum: "rnode_firmware_tbeam.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-beam",
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)",
firmware_filename_classic: "rnode_firmware_tbeam.zip",
firmware_filename_microreticulum: "rnode_firmware_tbeam.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-beam",
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)",
firmware_filename_classic: "rnode_firmware_tbeam_sx1262.zip",
firmware_filename_microreticulum: "rnode_firmware_tbeam_sx1262.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-beam",
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)",
firmware_filename_classic: "rnode_firmware_tbeam_sx1262.zip",
firmware_filename_microreticulum: "rnode_firmware_tbeam_sx1262.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-beam",
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 T-Beam Supreme",
id: ROM.PRODUCT_TBEAM_S_V1,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_DB,
name: "433 MHz (with SX1268 chip)",
},
{
id: ROM.MODEL_DC,
name: "868/915/923 MHz (with SX1262 chip)",
},
],
firmware_filename_classic: "rnode_firmware_tbeam_supreme.zip",
firmware_filename_microreticulum: "rnode_firmware_tbeam_supreme.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-beam-supreme",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tbeam_supreme.boot_app0",
"0x0": "rnode_firmware_tbeam_supreme.bootloader",
"0x10000": "rnode_firmware_tbeam_supreme.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tbeam_supreme.partitions",
},
},
},
{
name: "LilyGO T-Beam Supreme (V3)",
id: ROM.PRODUCT_TBEAM_S_V3,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_DB,
name: "433 MHz (with SX1268 chip)",
},
{
id: ROM.MODEL_DC,
name: "868/915/923 MHz (with SX1262 chip)",
},
],
firmware_filename_classic: "rnode_firmware_tbeam_supreme_v3.zip",
firmware_filename_microreticulum: "rnode_firmware_tbeam_supreme_v3.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-beam-supreme",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tbeam_supreme_v3.boot_app0",
"0x0": "rnode_firmware_tbeam_supreme_v3.bootloader",
"0x10000": "rnode_firmware_tbeam_supreme_v3.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tbeam_supreme_v3.partitions",
},
},
},
{
name: "LilyGO T-Deck",
id: ROM.PRODUCT_TDECK,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_D4,
name: "433 MHz (with SX1268 chip)",
},
{
id: ROM.MODEL_D9,
name: "868/915/923 MHz (with SX1262 chip)",
},
],
firmware_filename_classic: "rnode_firmware_tdeck.zip",
firmware_filename_microreticulum: "rnode_firmware_tdeck.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://lilygo.cc/en-us/products/t-deck",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_tdeck.boot_app0",
"0x0": "rnode_firmware_tdeck.bootloader",
"0x10000": "rnode_firmware_tdeck.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_tdeck.partitions",
},
},
},
{
name: "Xiao ESP32S3",
id: ROM.PRODUCT_XIAO_S3,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_DE,
name: "433 MHz",
},
{
id: ROM.MODEL_DD,
name: "868/915/923 MHz",
},
],
firmware_filename_classic: "rnode_firmware_xiao_esp32s3.zip",
firmware_filename_microreticulum: "rnode_firmware_xiao_esp32s3.zip",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://www.seeedstudio.com/XIAO-ESP32S3-p-5627.html",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_xiao_esp32s3.boot_app0",
"0x0": "rnode_firmware_xiao_esp32s3.bootloader",
"0x10000": "rnode_firmware_xiao_esp32s3.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_xiao_esp32s3.partitions",
},
},
},
{
name: "LilyGO T-Echo",
id: ROM.PRODUCT_TECHO,
platform: ROM.PLATFORM_NRF52,
models: [
{
id: ROM.MODEL_16,
name: "433 MHz",
},
{
id: ROM.MODEL_17,
name: "868 MHz / 915 MHz / 923 MHz",
},
],
firmware_filename_classic: "rnode_firmware_techo.zip",
hardware_info: "https://lilygo.cc/en-us/products/t-echo",
},
{
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",
},
],
firmware_filename_classic: "rnode_firmware_rak4631.zip",
hardware_info: "https://docs.rakwireless.com/product-categories/wisblock/rak4631/overview/",
},
{
name: "MeshAdventurer-S3",
id: ROM.PRODUCT_HMBRW,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_FE,
name: "433 MHz",
firmware_filename_classic: "rnode_firmware_meshadventurer_s3.zip",
firmware_filename_microreticulum: "rnode_firmware_meshadventurer_s3.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://git.rns.moscow/deuxyeux/MeshAdventurer-S3",
flash_config: {
flash_size: "16MB",
flash_files: {
"0xe000": "rnode_firmware_meshadventurer_s3.boot_app0",
"0x0": "rnode_firmware_meshadventurer_s3.bootloader",
"0x10000": "rnode_firmware_meshadventurer_s3.bin",
"0x8000": "rnode_firmware_meshadventurer_s3.partitions",
"0x210000": "console_image.bin",
},
},
},
{
id: ROM.MODEL_FE,
name: "868/915/923 MHz",
firmware_filename_classic: "rnode_firmware_meshadventurer_s3.zip",
firmware_filename_microreticulum: "rnode_firmware_meshadventurer_s3.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://git.rns.moscow/deuxyeux/MeshAdventurer-S3",
flash_config: {
flash_size: "16MB",
flash_files: {
"0xe000": "rnode_firmware_meshadventurer_s3.boot_app0",
"0x0": "rnode_firmware_meshadventurer_s3.bootloader",
"0x10000": "rnode_firmware_meshadventurer_s3.bin",
"0x8000": "rnode_firmware_meshadventurer_s3.partitions",
"0x210000": "console_image.bin",
},
},
},
{
id: ROM.MODEL_FE,
name: "RTNode Firmware",
firmware_filename_rtnode: "rtnode_meshadventurer_s3.zip",
firmware_repo_rtnode: "https://git.rns.moscow/deuxyeux/RTNode-HeltecV4",
hardware_info: "https://git.rns.moscow/deuxyeux/MeshAdventurer-S3",
flash_config: {
flash_size: "16MB",
flash_files: {
"0xe000": "boot_app0.bin",
"0x0": "bootloader.bin",
"0x10000": "firmware.bin",
"0x8000": "partitions.bin",
},
},
},
{
id: ROM.MODEL_FE,
name: "Meshtastic Firmware",
firmware_filename_custom: "meshtastic/latest/meshtastic_firmware_meshadventurer-s3.zip",
firmware_repo_custom: "https://git.rns.moscow/deuxyeux/meshtastic_firmware",
hardware_info: "https://git.rns.moscow/deuxyeux/MeshAdventurer-S3",
flash_config: {
flash_size: "16MB",
flash_files: {
"0x0000": "bootloader.bin",
"0x8000": "partitions.bin",
"0x10000": "firmware.bin",
},
},
},
],
},
{
name: "MeshAdventurer",
id: ROM.PRODUCT_HMBRW,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_FE,
name: "433 MHz",
},
{
id: ROM.MODEL_FE,
name: "433 MHz (Merged Binary)",
firmware_filename_classic: "rnode_firmware_meshadventurer_merged.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/chrismyers2000/MeshAdventurer",
flash_config: {
flash_size: "4MB",
flash_files: {
"0x0": "rnode_firmware_meshadventurer_merged.bin",
},
},
},
{
id: ROM.MODEL_FE,
name: "868/915/923 MHz (30dBm)",
},
{
id: ROM.MODEL_FD,
name: "868/915/923 MHz (33dBm)",
},
{
id: ROM.MODEL_FE,
name: "868/915/923 MHz (Merged Binary, 30dBm)",
firmware_filename_classic: "rnode_firmware_meshadventurer_merged.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/chrismyers2000/MeshAdventurer",
flash_config: {
flash_size: "4MB",
flash_files: {
"0x0": "rnode_firmware_meshadventurer_merged.bin",
},
},
},
{
id: ROM.MODEL_FD,
name: "868/915/923 MHz (Merged Binary, 33dBm)",
firmware_filename_classic: "rnode_firmware_meshadventurer_merged.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/chrismyers2000/MeshAdventurer",
flash_config: {
flash_size: "4MB",
flash_files: {
"0x0": "rnode_firmware_meshadventurer_merged.bin",
},
},
},
],
firmware_filename_classic: "rnode_firmware_meshadventurer.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/chrismyers2000/MeshAdventurer",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_meshadventurer.boot_app0",
"0x1000": "rnode_firmware_meshadventurer.bootloader",
"0x10000": "rnode_firmware_meshadventurer.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_meshadventurer.partitions",
},
},
},
{
name: "FakeTec (Promicro)",
id: ROM.PRODUCT_HMBRW,
platform: ROM.PLATFORM_NRF52,
models: [
{
id: ROM.MODEL_FE,
name: "863-928 MHz",
},
],
firmware_filename_classic: "rnode_firmware_promicro.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/gargomoma/fakeTec_pcb",
},
{
name: "Aethernode",
id: ROM.PRODUCT_HMBRW,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_FE,
name: "433 MHz",
firmware_filename_classic: "rnode_firmware_aethernode.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/ahedproductions/aethernode",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_aethernode.boot_app0",
"0x1000": "rnode_firmware_aethernode.bootloader",
"0x10000": "rnode_firmware_aethernode.bin",
"0x8000": "rnode_firmware_aethernode.partitions",
"0x210000": "console_image.bin",
},
},
},
{
id: ROM.MODEL_FE,
name: "Meshtastic Firmware",
firmware_filename_custom: "meshtastic/latest/meshtastic_firmware_aethernode.zip",
hardware_info: "https://github.com/ahedproductions/aethernode",
flash_config: {
flash_size: "4MB",
flash_files: {
"0x1000": "bootloader.bin",
"0x8000": "partitions.bin",
"0x10000": "firmware.bin",
},
},
},
],
},
{
name: "DIY-V1",
id: ROM.PRODUCT_HMBRW,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_FE,
name: "470-510 MHz",
},
{
id: ROM.MODEL_FE,
name: "863-928 MHz",
},
],
firmware_filename_classic: "rnode_firmware_diy_v1.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
hardware_info: "https://github.com/NanoVHF/Meshtastic-DIY",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_diy_v1.boot_app0",
"0x1000": "rnode_firmware_diy_v1.bootloader",
"0x10000": "rnode_firmware_diy_v1.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_diy_v1.partitions",
},
},
},
{
name: "RNode",
id: ROM.PRODUCT_RNODE,
platform: ROM.PLATFORM_ESP32,
models: [
{
id: ROM.MODEL_A2,
name: "Handheld v2.0 RNode, 410 - 525 MHz",
firmware_filename_classic: "rnode_firmware_ng20.zip",
firmware_filename_microreticulum: "rnode_firmware_ng20.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://unsigned.io/hardware/Original_RNode.html",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_ng21.boot_app0",
"0x1000": "rnode_firmware_ng21.bootloader",
"0x10000": "rnode_firmware_ng21.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_ng21.partitions",
},
},
},
{
id: ROM.MODEL_A7,
name: "Handheld v2.0 RNode, 820 - 1020 MHz",
firmware_filename_classic: "rnode_firmware_ng20.zip",
firmware_filename_microreticulum: "rnode_firmware_ng20.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://unsigned.io/hardware/Original_RNode.html",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_ng21.boot_app0",
"0x1000": "rnode_firmware_ng21.bootloader",
"0x10000": "rnode_firmware_ng21.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_ng21.partitions",
},
},
},
{
id: ROM.MODEL_A2,
name: "Handheld v2.1 RNode, 410 - 525 MHz",
firmware_filename_classic: "rnode_firmware_ng21.zip",
firmware_filename_microreticulum: "rnode_firmware_ng21.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://unsigned.io/hardware/RNode.html",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_ng21.boot_app0",
"0x1000": "rnode_firmware_ng21.bootloader",
"0x10000": "rnode_firmware_ng21.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_ng21.partitions",
},
},
},
{
id: ROM.MODEL_A7,
name: "Handheld v2.1 RNode, 820 - 1020 MHz",
firmware_filename_classic: "rnode_firmware_ng21.zip",
firmware_filename_microreticulum: "rnode_firmware_ng21.zip",
firmware_repo_classic: "https://git.rns.moscow/deuxyeux/RNode_Firmware",
firmware_repo_microreticulum: "https://github.com/attermann/microReticulum_Firmware",
hardware_info: "https://unsigned.io/hardware/RNode.html",
flash_config: {
flash_size: "4MB",
flash_files: {
"0xe000": "rnode_firmware_ng21.boot_app0",
"0x1000": "rnode_firmware_ng21.bootloader",
"0x10000": "rnode_firmware_ng21.bin",
"0x210000": "console_image.bin",
"0x8000": "rnode_firmware_ng21.partitions",
},
},
},
],
},
],
// Default config
configFrequency: 868825000,
configBandwidth: 125000,
configTxPower: 22,
configSpreadingFactor: 10,
configCodingRate: 7,
displayIntensity: 255,
displayTimeout: 0,
wifiMode: null,
wifiChannel: 1,
wifiSSID: "",
wifiPSK: "",
wifiIP: "",
wifiNM: "",
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,
],
},
// Flag to track if we're in the initial setup
isInitializing: true,
};
},
mounted() {
// Do not set default selections automatically - keep null by default
},
methods: {
downloadEraseUF2() {
const link = document.createElement('a');
link.href = 'firmware/misc/nrf_erase2.uf2';
link.download = '';
link.click();
},
async askForSerialPort() {
if(!navigator.serial){
alert("Web Serial is not supported in this browser");
return null;
}
// close any existing rnode connection
if(this.rnode){
await this.rnode.close();
this.rnode = null;
}
// ask user to select device
return await navigator.serial.requestPort({
filters: [],
});
},
async askForRNode() {
// ask for serial port
const serialPort = await this.askForSerialPort();
if(!serialPort){
return false;
}
// check if device is an rnode
this.rnode = await RNode.fromSerialPort(serialPort);
const isRNode = await this.rnode.detect();
if(!isRNode){
await this.rnode.close();
alert("Selected device is not an RNode!");
return false;
}
return this.rnode;
},
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...");
try {
await serialPort.close();
} catch(e) {
console.log("failed to close serial port, ignoring...", e);
}
},
async flashEsp32() {
// ensure ESPLoader is available
if(!window.ESPLoader){
alert("esptool-js could not be loaded.");
return;
}
// ensure flash config is known
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;
}
this.isFlashing = true;
this.flashingProgress = 0;
let transport;
try {
// read zip file
const blobReader = new window.zip.BlobReader(file);
const zipReader = new window.zip.ZipReader(blobReader);
const zipEntries = await zipReader.getEntries();
const filesToFlash = [];
for(const [address, filename] of Object.entries(flashConfig.flash_files)){
const zipEntry = zipEntries.find(e => e.filename === filename);
if(!zipEntry) throw filename + " not found in firmware file!";
const blob = await zipEntry.getData(new window.zip.BlobWriter());
const data = await this.readAsBinaryString(blob); // deprecated, works for now
filesToFlash.push({ address: parseInt(address), data });
}
// init transport and esploader
transport = new window.Transport(serialPort, true);
const esploader = new window.ESPLoader({
transport,
baudrate: 921600,
debugLogging: false,
enableTracing: false,
terminal: {
clean() {},
writeLine(data) { console.log(data); },
write(data) { console.log(data); },
},
});
// completely disable TRACE logging
transport.trace = () => {};
// 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 percent = (written / total) * 100;
this.flashingProgress = Math.floor(percent);
},
});
// reboot device
await transport.setDTR(false);
await new Promise(resolve => setTimeout(resolve, 100));
await transport.setDTR(true);
alert("Firmware has been flashed!");
} catch(e) {
alert("Firmware flashing failed: " + e);
console.error(e);
} finally {
this.isFlashing = false;
// release reader/writer locks before closing port
try {
if(transport?.reader){
await transport.reader.cancel();
transport.reader.releaseLock();
}
if(transport?.writer){
transport.writer.releaseLock();
}
console.log("Closing serial port...");
await serialPort.close();
} catch(e) {
console.warn("Failed to close serial port, ignoring...", e);
}
}
},
async eraseEsp32() {
if (!window.ESPLoader) {
alert("esptool-js could not be loaded.");
return;
}
// ask for serial port
const serialPort = await this.askForSerialPort();
if (!serialPort) return;
this.isFlashing = true;
let transport, esploader;
try {
transport = new window.Transport(serialPort, true);
esploader = new window.ESPLoader({
transport: transport,
baudrate: 921600,
debugLogging: false,
enableTracing: false,
terminal: {
clean() {},
writeLine(data) { console.log(data); },
write(data) { console.log(data); },
},
});
// completely disable TRACE logging
transport.trace = () => {};
await esploader.main();
await esploader.eraseFlash();
alert("Flash erase completed!");
} catch (e) {
alert("Flash erase failed: " + e);
console.log(e);
} finally {
this.isFlashing = false;
// properly release streams before closing
try {
if (transport?.reader) {
await transport.reader.cancel();
transport.reader.releaseLock();
}
if (transport?.writer) {
transport.writer.releaseLock();
}
console.log("Closing serial port...");
await serialPort.close();
} catch (e) {
console.warn("Failed to close serial port, ignoring...", e);
}
}
},
async detect() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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(),
wifi_mode: await rnode.getWifiMode(),
});
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 rnode
const rnode = await this.askForRNode();
if(!rnode){
return;
}
// reboot
await rnode.reset();
await rnode.close();
// done
alert("Board is rebooting!");
},
frameBufferToCanvas(framebuffer, width, height, backgroundColour, foregroundColour) {
// create canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
// fill the canvas with background colour
const ctx = canvas.getContext('2d');
ctx.fillStyle = backgroundColour;
ctx.fillRect(0, 0, width, height);
// draw foreground pixels where bits are 1
ctx.fillStyle = foregroundColour;
for(let y = 0; y < height; y++){
for(let x = 0; x < width; x++){
const byteIndex = Math.floor((y * width + x) / 8);
const bitIndex = x % 8;
const bit = (framebuffer[byteIndex] >> (7 - bitIndex)) & 1;
if(bit){
ctx.fillRect(x, y, 1, 1);
}
}
}
return canvas;
},
rnodeDisplayBufferToPng(displayBuffer) {
// get display area and stat area
const displayArea = displayBuffer.slice(0, 512);
const statArea = displayBuffer.slice(512, 1024);
// create canvas from frame buffers
const displayCanvasOriginal = this.frameBufferToCanvas(displayArea, 64, 64, "#000000", "#FFFFFF");
const statCanvasOriginal = this.frameBufferToCanvas(statArea, 64, 64, "#000000", "#FFFFFF");
// create horizontal canvas to show display area and stat area next to each other
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 64;
// draw canvases to final canvas
const canvasCtx = canvas.getContext('2d');
canvasCtx.imageSmoothingEnabled = false;
canvasCtx.drawImage(displayCanvasOriginal, 0, 0, 64, 64);
canvasCtx.drawImage(statCanvasOriginal, 64, 0, 64, 64);
// create scaled canvas
const scaleFactor = 4;
const scaledCanvas = document.createElement('canvas');
scaledCanvas.width = canvas.width * scaleFactor;
scaledCanvas.height = canvas.height * scaleFactor;
// scale original canvas onto new canvas
const scaledCtx = scaledCanvas.getContext('2d');
scaledCtx.imageSmoothingEnabled = false;
scaledCtx.drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height);
// convert canvas to png
return scaledCanvas.toDataURL("image/png");
},
async readDisplay() {
// ask for rnode
const rnode = await this.askForRNode();
if(!rnode){
return;
}
// read display
const displayBuffer = await rnode.readDisplay();
// disconnect from rnode
await rnode.close();
// update ui
this.rnodeDisplayImage = this.rnodeDisplayBufferToPng(displayBuffer);
},
async dumpEeprom() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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 rnode
const rnode = await this.askForRNode();
if(!rnode){
return;
}
// ask user to confirm
if(!confirm("Are you sure you want to wipe the eeprom on this device? This will take about 30 seconds. An alert will show when the eeprom wipe has finished.")){
await rnode.close();
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 rnode
const rnode = await this.askForRNode();
if(!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;
}
// 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...");
this.isProvisioning = true;
try {
// 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();
alert("device has been provisioned!");
} catch(e) {
console.log(e);
alert("failed to provision, please try again");
}
this.isProvisioning = false;
await rnode.close();
},
async setFirmwareHash() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
this.isSettingFirmwareHash = true;
try {
// 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...");
}
alert("firmware hash has been set!");
} catch(e) {
console.log(e);
alert("failed to set firmware hash, please try again")
}
this.isSettingFirmwareHash = false;
// done
await rnode.close();
},
async enableTncMode() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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 rnode
const rnode = await this.askForRNode();
if(!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 enableBluetooth() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// enable bluetooth
console.log("enabling bluetooth");
await rnode.enableBluetooth();
console.log("enabling bluetooth: done");
alert("Bluetooth has been enabled!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async disableBluetooth() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// disable bluetooth
console.log("disabling bluetooth");
await rnode.disableBluetooth();
console.log("disabling bluetooth: done");
alert("Bluetooth has been disabled!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async startBluetoothPairing() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// start bluetooth pairing
try {
console.log("start bluetooth pairing");
await rnode.startBluetoothPairing(async (pin) => {
alert("Bluetooth Pairing Pin: " + pin);
await rnode.close();
});
console.log("start bluetooth pairing: done");
} catch(error) {
alert(error);
}
// tell user device is in pairing mode, and how to pair
alert([
"- RNode is in Bluetooth Pairing Mode for 30 seconds.",
"- Close this alert before performing the next steps.",
"- Open bluetooth settings on your Android device.",
"- Click pair on the RNode device that shows up.",
"- Bluetooth pin will shown on your RNode screen and on this page.",
].join("\n"));
},
async setWiFiMode() {
// ask for rnode
const rnode = await this.askForRNode();
if (!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;
}
// normalize mode to number (safety)
const mode = Number(this.wifiMode);
console.log("setting wifi mode to", mode);
await rnode.enableWiFiMode(mode);
console.log("setting wifi mode: done");
const modeMap = {
0: "OFF",
1: "STATION",
2: "AP"
};
const modeLabel = modeMap[mode] ?? "UNKNOWN";
alert("WiFi mode has been set to " + modeLabel + "!");
// update UI if needed
this.wifiMode = mode;
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async setWiFiChannel() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// set wifi channel
console.log("setting wifi channel to", this.wifiChannel);
await rnode.setWiFiChannel(this.wifiChannel);
console.log("setting wifi channel: done");
alert("WiFi channel has been set to " + this.wifiChannel + "!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async setWiFiSSID() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// set wifi SSID
console.log("setting wifi SSID to", this.wifiSSID);
await rnode.setWiFiSSID(this.wifiSSID);
console.log("setting wifi SSID: done");
alert("WiFi SSID has been set to " + this.wifiSSID + "!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async disableWiFiMode() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// disable wifi mode
console.log("disabling wifi mode");
await rnode.disableWiFiMode();
console.log("disabling wifi mode: done");
alert("WiFi mode has been disabled!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async setWiFiPSK() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// set wifi psk
console.log("setting wifi psk to", this.wifiPSK);
await rnode.setWiFiPSK(this.wifiPSK);
console.log("setting wifi psk: done");
alert("WiFi PSK has been set!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async unsetWiFiSSID() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// unset wifi SSID by setting it to "None"
console.log("unsetting wifi SSID");
await rnode.setWiFiSSID("None");
console.log("unsetting wifi SSID: done");
alert("WiFi SSID has been unset!");
// update UI
this.wifiSSID = "None";
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async unsetWiFiPSK() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// unset wifi psk by setting it to "None"
console.log("unsetting wifi psk");
await rnode.setWiFiPSK(null);
console.log("unsetting wifi psk: done");
alert("WiFi PSK has been unset!");
// update UI
this.wifiPSK = "";
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async unsetWiFiIP() {
const rnode = await this.askForRNode();
if (!rnode) return;
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;
}
console.log("unsetting wifi ip");
await rnode.setWiFiIP(null);
console.log("unsetting wifi ip: done");
this.wifiIP = "";
await Utils.sleepMillis(1000);
await rnode.close();
},
async unsetWiFiNM() {
const rnode = await this.askForRNode();
if (!rnode) return;
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;
}
console.log("unsetting wifi netmask");
await rnode.setWiFiNM(null);
console.log("unsetting wifi netmask: done");
this.wifiNM = "";
await Utils.sleepMillis(1000);
await rnode.close();
},
async setWiFiIP() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// set wifi ip
console.log("setting wifi ip to", this.wifiIP);
await rnode.setWiFiIP(this.wifiIP);
console.log("setting wifi ip: done");
alert("WiFi IP has been set to " + this.wifiIP + "!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async setWiFiNM() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// set wifi nm
console.log("setting wifi nm to", this.wifiNM);
await rnode.setWiFiNM(this.wifiNM);
console.log("setting wifi nm: done");
alert("WiFi Netmask has been set to " + this.wifiNM + "!");
// done
await Utils.sleepMillis(1000);
await rnode.close();
},
async setDisplayRotation(rotation) {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// configure
console.log("setting display rotation");
await rnode.setDisplayRotation(rotation);
console.log("setting display rotation: done");
// done
await rnode.close();
},
async setDisplayIntensity(intensity) {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// configure
console.log("setting display intensity");
await rnode.setDisplayIntensity(intensity);
console.log("setting display intensity: done");
// done
await rnode.close();
},
async setDisplayTimeout(timeout) {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// configure
console.log("setting display timeout");
await rnode.setDisplayTimeout(timeout);
console.log("setting display timeout: done");
// done
await rnode.close();
},
async startDisplayReconditioning() {
// ask for rnode
const rnode = await this.askForRNode();
if(!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;
}
// configure
console.log("starting display reconditioning");
await rnode.startDisplayReconditioning();
console.log("starting display reconditioning: done");
// done
await rnode.close();
},
async readAsBinaryString(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsBinaryString(blob);
});
},
},
computed: {
recommendedFirmwareFilename() {
return this.selectedModel?.firmware_filename_classic ?? this.selectedProduct?.firmware_filename_classic;
},
recommendedFirmwareFilenameMicroReticulum() {
return this.selectedModel?.firmware_filename_microreticulum ?? this.selectedProduct?.firmware_filename_microreticulum;
},
recommendedFirmwareFilenameCustom() {
return this.selectedModel?.firmware_filename_custom ?? this.selectedProduct?.firmware_filename_custom;
},
recommendedFirmwareFilenameRTNode() {
return this.selectedModel?.firmware_filename_rtnode ?? this.selectedProduct?.firmware_filename_rtnode;
},
FirmwareRepoClassic() {
return this.selectedModel?.firmware_repo_classic ?? this.selectedProduct?.firmware_repo_classic;
},
FirmwareRepoMicroReticulum() {
return this.selectedModel?.firmware_repo_microreticulum ?? this.selectedProduct?.firmware_repo_microreticulum;
},
FirmwareRepoCustom() {
return this.selectedModel?.firmware_repo_custom ?? this.selectedProduct?.firmware_repo_custom;
},
FirmwareRepoRTNode() {
return this.selectedModel?.firmware_repo_rtnode ?? this.selectedProduct?.firmware_repo_rtnode;
},
HardwareInfo() {
return this.selectedModel?.hardware_info ?? this.selectedProduct?.hardware_info;
},
isSsidValid() {
if (!this.wifiSSID) return false;
const encoder = new TextEncoder();
const bytes = encoder.encode(this.wifiSSID);
return bytes.length >= 1 && bytes.length <= 32;
},
isPskValid() {
if (!this.wifiPSK) return false;
const encoder = new TextEncoder();
const bytes = encoder.encode(this.wifiPSK);
return bytes.length >= 8;
},
isIpValid() {
if (!this.wifiIP) return false;
const parts = this.wifiIP.split(".");
if (parts.length !== 4) return false;
for (const part of parts) {
if (part === "") return false;
const num = Number(part);
if (!Number.isInteger(num)) return false;
if (num < 0 || num > 255) return false;
}
return true;
},
isNetmaskValid() {
if (!this.wifiNM) return false;
const parts = this.wifiNM.split(".");
if (parts.length !== 4) return false;
for (const part of parts) {
if (part === "") return false;
const num = Number(part);
if (!Number.isInteger(num)) return false;
if (num < 0 || num > 255) return false;
}
return true;
},
},
watch: {
selectedProduct() {
// reset selected model when changing selected product, but not during initialization
if (!this.isInitializing) {
this.selectedModel = null;
}
},
},
}).mount('#app');
</script>
</body>
</html>