initial commit
375
config.json
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
{
|
||||
"basePath": "./firmware",
|
||||
"role": {
|
||||
"gui": {
|
||||
"icon": "gradient",
|
||||
"title": "Client GUI",
|
||||
"tooltip": "all your device settings are saved on internal flash"
|
||||
},
|
||||
"guiSD": {
|
||||
"icon": "gradient",
|
||||
"title": "Client GUI: data on SD card",
|
||||
"tooltip": "all your device settings are saved on SD card"
|
||||
},
|
||||
"companionBle": {
|
||||
"icon": "smartphone",
|
||||
"class": "primary-text",
|
||||
"title": "Companion radio: Bluetooth",
|
||||
"tooltip": "Chat via mobile phone App or Web Client"
|
||||
},
|
||||
"companionUsb": {
|
||||
"icon": "usb",
|
||||
"title": "Companion radio: USB",
|
||||
"tooltip": "Chat via Web client or command line client"
|
||||
},
|
||||
"repeater": {
|
||||
"icon": "cell_tower",
|
||||
"title": "Repeater",
|
||||
"tooltip": "Special role just for routing packets. Configured via <b>Console</b> on flasher main page"
|
||||
},
|
||||
"roomServer": {
|
||||
"icon": "forum",
|
||||
"title": "Room Server",
|
||||
"tooltip": "Special role for local room and routing packets. Configured via <b>Console</b> on flasher main page"
|
||||
}
|
||||
},
|
||||
"device": [
|
||||
{
|
||||
"name": "Lilygo T-Deck",
|
||||
"tooltip": "<img class='device' src='/img/lilygo_tdeck.png'>",
|
||||
"type": "esp32",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "gui",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "RippleUltra-TDeck-v6.0-beta21-merged.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "RippleUltra-TDeck-v6.0-beta21.bin",
|
||||
"title": "App firmware bin (use with m5 booloader)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "guiSD",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "RippleUltra-TDeck-SD-v6.0-beta21-merged.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "RippleUltra-TDeck-SD-v6.0-beta21.bin",
|
||||
"title": "App firmware bin (use with m5 booloader)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Lilygo T3 S3",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='/img/lilygo_t3s3.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "companionUsb",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "LilyGo_T3S3_sx1262_companion_radio_usb.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "companionBle",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "LilyGo_T3S3_sx1262_companion_radio_ble.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "LilyGo_T3S3_sx1262_Repeater.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Heltec v2",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='/img/heltec_v3.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "companionUsb",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_v2_companion_radio_usb.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_v2_repeater.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Heltec v3",
|
||||
"type": "esp32",
|
||||
"tooltip": "<img class='device' src='/img/heltec_v3.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "companionUsb",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_v3_companion_radio_usb.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "companionBle",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_v3_companion_radio_ble.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_v3_repeater.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "roomServer",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_v3_room_server.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Heltec T114",
|
||||
"type": "nrf52",
|
||||
"tooltip": "<img class='device' src='/img/heltec_t114.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_T114_repeater.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "Heltec_T114_repeater.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "roomServer",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Heltec_T114_room_server.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "Heltec_T114_room_server.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RAK Wireless WisBlock / WisMesh (RAK 4631)",
|
||||
"type": "nrf52",
|
||||
"tooltip": "<img class='device' src='/img/rak_4631.png'>",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "companionUsb",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "RAK_4631_companion_radio_usb.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "RAK_4631_companion_radio_usb.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "companionBle",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "RAK_4631_companion_radio_ble.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "RAK_4631_companion_radio_ble.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "RAK_4631_Repeater.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "RAK_4631_Repeater.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "roomServer",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "RAK_4631_room_server.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "RAK_4631_room_server.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Seeed Studio SenseCAP T1000-E",
|
||||
"tooltip": "<img class='device' src='/img/seeed_t1000e.png'>",
|
||||
"type": "nrf52",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "companionBle",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Seeed_T1000e_companion_radio_ble.zip",
|
||||
"title": "firmware OTA zip"
|
||||
},
|
||||
{
|
||||
"type": "download",
|
||||
"name": "Seeed_T1000e_companion_radio_ble.uf2",
|
||||
"title": "firmware uf2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Seeed Studio Xiao C3",
|
||||
"tooltip": "<img class='device' src='/img/xiao_s3.png'>",
|
||||
"type": "esp32",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "repeater",
|
||||
"title": "Repeater (Semtech SX1262)",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Xiao_C3_Repeater_sx1262.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "repeater",
|
||||
"title": "Repeater (Semtech SX1268)",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Xiao_C3_Repeater_sx1268.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Seeed Studio Xiao S3 WIO",
|
||||
"tooltip": "<img class='device' src='/img/xiao_s3.png'>",
|
||||
"type": "esp32",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Xiao_S3_WIO_Repeater.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UnitEng Station G2",
|
||||
"tooltip": "<img class='device' src='/img/station_g2.png'>",
|
||||
"type": "esp32",
|
||||
"firmware": [
|
||||
{
|
||||
"role": "repeater",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash",
|
||||
"name": "Station_G2_repeater.bin",
|
||||
"title": "Combined app+partition+bootloader firmware bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1
css/beer.css
Normal file
0
css/flasher.css
Normal file
397
flasher.js
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
import "./lib/beer.min.js";
|
||||
import { createApp, reactive, ref, nextTick } from "./lib/vue.min.js";
|
||||
import { Dfu } from "./lib/dfu.js";
|
||||
import { ESPLoader, Transport, HardReset } from "./lib/esp32.js";
|
||||
import { SerialConsole } from './lib/console.js';
|
||||
|
||||
const res = await fetch('./config.json');
|
||||
const config = await res.json();
|
||||
const commandReference = {
|
||||
'set freq ': 'Set frequency {Mhz}',
|
||||
'time ': 'Set time {epoch-secs}',
|
||||
'erase': 'Erase filesystem',
|
||||
'advert': 'Send Advertisment packet',
|
||||
'reboot': 'Reboot device',
|
||||
'clock': 'Display current time',
|
||||
'password ': 'Set new password',
|
||||
'log': 'Ouput log',
|
||||
'log start': 'Start packet logging to file system',
|
||||
'log stop': 'Stop packet logging to file system',
|
||||
'log erase': 'Erase the packet logs from file system',
|
||||
'ver': 'Show device version',
|
||||
'set af ': 'Set Air-time factor',
|
||||
'set tx ': 'Set Tx power {dBm}',
|
||||
'set repeat ': 'Set repeater mode {on|off}',
|
||||
'set advert.interval ': 'Set advert rebroadcast interval {minutes}',
|
||||
'set guest.password ': 'Set guest password',
|
||||
'set name ': 'Set advertisement name',
|
||||
'set lat': 'Set the advertisement map latitude',
|
||||
'set lon': 'Set the advertisement map longitude',
|
||||
};
|
||||
|
||||
function setup() {
|
||||
const consoleEditBox = ref();
|
||||
const consoleWindow = ref();
|
||||
|
||||
const selected = reactive({
|
||||
device: null,
|
||||
firmware: null,
|
||||
wipe: false,
|
||||
port: null
|
||||
});
|
||||
|
||||
const flashing = reactive({
|
||||
instance: null,
|
||||
active: false,
|
||||
percentage: 0,
|
||||
log: '',
|
||||
error: '',
|
||||
dfuComplete: false,
|
||||
});
|
||||
|
||||
const serialCon = reactive({
|
||||
instance: null,
|
||||
opened: false,
|
||||
content: '',
|
||||
edit: '',
|
||||
});
|
||||
|
||||
window.app = { selected, flashing, serialCon };
|
||||
|
||||
const log = {
|
||||
clean() { flashing.log = '' },
|
||||
write(data) { flashing.log += data },
|
||||
writeLine(data) { flashing.log += data + '\n' }
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
const flasherCleanup = async () => {
|
||||
const port = selected.port;
|
||||
flashing.active = false;
|
||||
flashing.log = '';
|
||||
flashing.error = '';
|
||||
flashing.dfuComplete = false;
|
||||
flashing.percentage = 0;
|
||||
selected.firmware = null;
|
||||
selected.wipe = false;
|
||||
selected.device = null;
|
||||
if(flashing.instance instanceof ESPLoader) {
|
||||
await flashing.instance?.hr.reset();
|
||||
await flashing.instance?.transport?.disconnect();
|
||||
}
|
||||
flashing.instance = null;
|
||||
}
|
||||
|
||||
const openSerialCon = async() => {
|
||||
const port = selected.port = await navigator.serial.requestPort();
|
||||
const serialConsole = serialCon.instance = new SerialConsole(port);
|
||||
serialCon.content = 'Welcome to MeshCore serial console.\n';
|
||||
serialCon.content += 'If you came here right after flashing, please restart your device.\n';
|
||||
serialCon.content += 'Click on the cursor to get all supported commands.\n\n';
|
||||
serialConsole.onOutput = (text) => {
|
||||
serialCon.content += text;
|
||||
};
|
||||
serialConsole.connect();
|
||||
serialCon.opened = true;
|
||||
await nextTick();
|
||||
|
||||
consoleEditBox.value.focus();
|
||||
}
|
||||
|
||||
const closeSerialCon = async() => {
|
||||
serialCon.opened = false;
|
||||
await serialCon.instance.disconnect();
|
||||
}
|
||||
|
||||
const sendCommand = async(text) => {
|
||||
const consoleEl = consoleWindow.value;
|
||||
serialCon.edit = '';
|
||||
await serialCon.instance.sendCommand(text);
|
||||
setTimeout(() => consoleEl.scrollTop = consoleEl.scrollHeight, 100);
|
||||
}
|
||||
|
||||
const dfuMode = async() => {
|
||||
await Dfu.forceDfuMode(await navigator.serial.requestPort({}))
|
||||
flashing.dfuComplete = true;
|
||||
}
|
||||
|
||||
const flashDevice = async() => {
|
||||
const device = selected.device;
|
||||
const firmware = selected.firmware;
|
||||
const flashFile = firmware.files.find(f => f.type === 'flash');
|
||||
if(!flashFile) {
|
||||
alert('Cannot find configuration for flash file! please report this to Discord.')
|
||||
flasherCleanup();
|
||||
return;
|
||||
}
|
||||
const url = `${config.basePath}/${flashFile.name}`;
|
||||
const resp = await fetch(url);
|
||||
const port = selected.port = await navigator.serial.requestPort({});
|
||||
|
||||
if(device.type === 'esp32') {
|
||||
let esploader;
|
||||
let fileData;
|
||||
let transport;
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
fileData = await new Promise(async (resolve) => {
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.readAsBinaryString(await resp.blob());
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
flashing.error = `Cannot read flash file: ${e}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const flashOptions = {
|
||||
terminal: log,
|
||||
compress: true,
|
||||
eraseAll: selected.wipe,
|
||||
flashSize: 'keep',
|
||||
flashMode: 'keep',
|
||||
flashFreq: 'keep',
|
||||
baudrate: 115200,
|
||||
romBaudrate: 115200,
|
||||
enableTracing: false,
|
||||
fileArray: [{
|
||||
data: fileData,
|
||||
address: 0
|
||||
}],
|
||||
reportProgress: async (fileIndex, written, total) => {
|
||||
flashing.percentage = (written / total) * 100;
|
||||
|
||||
// we're done with this file
|
||||
if (written === total) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
flashing.active = true;
|
||||
transport = new Transport(port, true);
|
||||
flashOptions.transport = transport
|
||||
flashing.instance = esploader = new ESPLoader(flashOptions);
|
||||
esploader.hr = new HardReset(transport);
|
||||
await esploader.main();
|
||||
await esploader.flashId();
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
flashing.error = `Failed to initialize. Did you place the device into firmware download mode? Detail: ${e}`;
|
||||
esploader = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await esploader.writeFlash(flashOptions);
|
||||
await esploader.after();
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
flashing.error = `ESP32 flashing failed: ${e}`;
|
||||
await esploader.hardReset();
|
||||
await transport.disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(device.type === 'nrf52') {
|
||||
const dfu = this.flashing.instance = new Dfu(port, selected.wipe);
|
||||
|
||||
const zipFile = await resp.blob();
|
||||
flashing.active = true;
|
||||
|
||||
try {
|
||||
await dfu.dfuUpdate(zipFile, async (progress) => {
|
||||
flashing.percentage = progress;
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
flashing.error = `nRF flashing failed: ${e}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
consoleEditBox, consoleWindow,
|
||||
config, selected, flashing,
|
||||
flashDevice, flasherCleanup, dfuMode,
|
||||
serialCon, openSerialCon, sendCommand, closeSerialCon,
|
||||
refresh, commandReference
|
||||
}
|
||||
}
|
||||
|
||||
const template = `
|
||||
<div class="flash-container">
|
||||
<div v-if="flashing.active">
|
||||
<header>
|
||||
<nav>
|
||||
<i>developer_board</i>
|
||||
<span class="small">{{ selected.device.name }}</span>
|
||||
<i>chevron_right</i>
|
||||
<i>{{ selected.firmware.icon }}</i>
|
||||
<span class="small">{{ selected.firmware.title }}</span>
|
||||
</nav>
|
||||
</header>
|
||||
<article v-if="flashing.error">
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<h6>Flashing failed!</h6>
|
||||
<p><span>{{ flashing.error }}</span></p>
|
||||
<p><button @click="refresh()">Retry</button></p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article v-else>
|
||||
<div class="row">
|
||||
<div class="max" v-if="flashing.percentage < 100">
|
||||
<h6><progress class="circle small"></progress> Flashing...</h6>
|
||||
<p>Please do not disconnect the device</p>
|
||||
</div>
|
||||
<div class="max" v-else=>
|
||||
<h6>Flashing complete!</h6>
|
||||
<p>
|
||||
<button @click="flasherCleanup()">Close</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="autoscroller">
|
||||
<pre class="term" v-if="flashing.terminal">{{ flashing.terminal }}</pre>
|
||||
</div>
|
||||
<nav>
|
||||
<progress :value="flashing.percentage" max="100"></progress>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="selected.firmware">
|
||||
<header>
|
||||
<nav>
|
||||
<button class="circle transparent" @click="selected.firmware = null"><i>arrow_back</i></button>
|
||||
<i>developer_board</i>
|
||||
<a class="small" href="javascript:;" @click="selected.firmware = null">{{ selected.device.name }}</a>
|
||||
<i>chevron_right</i>
|
||||
<i>{{ selected.firmware.icon }}</i>
|
||||
<span class="small">{{ selected.firmware.desc }}</span>
|
||||
</nav>
|
||||
<nav class="no-margin">
|
||||
<h6 class="small max">Install options</h6>
|
||||
</nav>
|
||||
</header>
|
||||
<ul class="list border" v-if="selected.device.type === 'esp32'">
|
||||
<li>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="selected.wipe">
|
||||
<span>Erase device</span>
|
||||
<div class="tooltip right max">
|
||||
DO NOT carry out a full erase if you are simply updating your MeshCore device, otherwise it will erase your MeshCore identity for that device.
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<button @click="dfuMode" :disabled="flashing.dfuComplete" v-if="selected.device.type === 'nrf52'">
|
||||
<i>{{ flashing.dfuComplete ? 'check' : 'code' }}</i>
|
||||
<span>{{ flashing.dfuComplete ? 'DFU mode active' : 'Enter DFU mode' }}</span>
|
||||
<div class="tooltip right max">
|
||||
Enter DFU mode - this mode enables you to flash your firmware.
|
||||
If you did not trigger the DFU mode manually, please click this button.
|
||||
</div>
|
||||
</button>
|
||||
<div class="medium-space"></div>
|
||||
<nav class="small-margin">
|
||||
<button @click="flashDevice">
|
||||
<i>bolt</i>
|
||||
<span>Flash!</span>
|
||||
<div class="tooltip right max">
|
||||
Upload the firmware into your device. Existing firwmare will get overwritten.
|
||||
<span v-if="selected.device.type === 'nrf52'">If you did not trigger DFU mode manually, use the <b>Enter DFU mode</b> before flashing</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="max"></div>
|
||||
<button data-ui="#down" class="active">
|
||||
<i>download</i>
|
||||
<span>Download</span><i>arrow_drop_down</i>
|
||||
<menu class="no-wrap" id="down" data-ui="#down">
|
||||
<li v-for="file in selected.firmware.files">
|
||||
<a data-ui="menu-selector" :href="config.basePath + '/' + file.name" download>{{ file.title }}</a>
|
||||
</li>
|
||||
</menu>
|
||||
<div class="tooltip left max">Download a copy of the firmware files for use with other flashers</div>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div v-else-if="selected.device">
|
||||
<header>
|
||||
<nav>
|
||||
<button class="circle transparent" @click="selected.device = null"><i>arrow_back</i></button>
|
||||
<i>developer_board</i>
|
||||
<span>{{ selected.device.name }}</span>
|
||||
</nav>
|
||||
<nav class="no-margin">
|
||||
<h6 class="small max">Choose role</h6>
|
||||
</nav>
|
||||
</header>
|
||||
<ul class="list border">
|
||||
<li v-for="firmware in selected.device.firmware" :class="firmware.class || config.role[firmware.role].class || ''">
|
||||
<button class="transparent" @click="selected.firmware = firmware">
|
||||
<i>{{ firmware.icon || config.role[firmware.role].icon }}</i>
|
||||
<span>{{ firmware.title || config.role[firmware.role].title }}</span>
|
||||
<div class="tooltip right max" v-if="firmware.tooltip || config.role[firmware.role].tooltip" v-html="firmware.tooltip || config.role[firmware.role].tooltip"></div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<header>
|
||||
<nav>
|
||||
<i>bolt</i>
|
||||
<h5 class="small max">MeshCore flasher</h5>
|
||||
<button class="transparent" @click="openSerialCon()">
|
||||
<i>terminal</i>
|
||||
<span>Console</span>
|
||||
<div class="tooltip left max">Open serial console to manage Routers and Room servers via serial terminal</div>
|
||||
</button>
|
||||
</nav>
|
||||
<nav class="no-margin">
|
||||
<h6 class="small max">Choose device</h6>
|
||||
</nav>
|
||||
</header>
|
||||
<ul class="list border">
|
||||
<li v-for="device in config.device">
|
||||
<button class="transparent" @click="selected.device = device">
|
||||
<i>developer_board</i>
|
||||
<span>{{ device.name }}</span>
|
||||
<div class="tooltip right max" v-if="device.tooltip" v-html="device.tooltip"></div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="serialCon.opened" class="overlay active">
|
||||
<datalist id="command-db">
|
||||
<option v-for="(desc, command) in commandReference" :value="command">{{ desc }}</option>
|
||||
</datalist>
|
||||
<header>
|
||||
<nav>
|
||||
<button class="circle transparent" @click="closeSerialCon()"><i>arrow_back</i></button>
|
||||
<h6 class="small max">Serial Console</h6>
|
||||
</nav>
|
||||
</header>
|
||||
<pre class="console" @click="consoleEditBox.focus()" ref="consoleWindow">
|
||||
<code>{{ serialCon.content }}</code>
|
||||
<div class="holder">
|
||||
<span>></span>
|
||||
<input ref="consoleEditBox" class="console-input" type="text" v-model="serialCon.edit" @keydown.enter.prevent="sendCommand(serialCon.edit)" list="command-db">
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
createApp({ setup, template }).mount('#flasher');
|
||||
BIN
img/heltec_t114.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
img/heltec_v3.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
img/lilygo_t3s3.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
img/lilygo_tdeck.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
img/meshcore-logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
img/rak_4631.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
img/seeed_t1000e.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
img/station_g2.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
img/xiao_s3.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
17
index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshCore flasher</title>
|
||||
<link href="./css/beer.css" rel="stylesheet">
|
||||
<link href="./css/flasher.css" rel="stylesheet">
|
||||
<script type="module" src="./flasher.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="dark">
|
||||
<div id="flasher"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
lib/beer.min.js
vendored
Normal file
97
lib/console.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
function delay(msecs) {
|
||||
return new Promise((resolve) => setTimeout(resolve, msecs));
|
||||
}
|
||||
class LineBreakTransformer {
|
||||
chunks = '';
|
||||
port = null;
|
||||
|
||||
transform(chunk, controller) {
|
||||
// Append new chunks to existing chunks.
|
||||
this.chunks += chunk;
|
||||
// For each line breaks in chunks, send the parsed lines out.
|
||||
const lines = this.chunks.split('\r\n');
|
||||
this.chunks = lines.pop();
|
||||
lines.forEach((line) => controller.enqueue(line + '\r\n'));
|
||||
}
|
||||
|
||||
flush(controller) {
|
||||
// When the stream is closed, flush any remaining chunks out.
|
||||
controller.enqueue(this.chunks);
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialConsole {
|
||||
connected = false;
|
||||
constructor(port) {
|
||||
this.port = port;
|
||||
this.controller = new AbortController();
|
||||
this.signal = this.controller.signal;
|
||||
this.onOutput = (text) => {
|
||||
console.log(text);
|
||||
};
|
||||
}
|
||||
|
||||
async connect() {
|
||||
try {
|
||||
await this.port.open({ baudRate: 115200 });
|
||||
this.connected = true;
|
||||
await this.port.readable
|
||||
.pipeThrough(new TextDecoderStream(), { signal: this.signal })
|
||||
.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: (chunk) => {
|
||||
this.addLine(chunk.replace('\r', ''));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Check AFTER the pipeTo has completed (or been aborted)
|
||||
if (!this.signal.aborted) {
|
||||
this.addLine('\n\n*** Terminal disconnected');
|
||||
this.connected = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.addLine(`\n\n*** Terminal disconnected: ${e}`);
|
||||
this.connected = false;
|
||||
} finally {
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
addLine(text) {
|
||||
this.onOutput(text);
|
||||
}
|
||||
|
||||
async sendCommand(command) {
|
||||
const encoder = new TextEncoder();
|
||||
const writer = this.port.writable.getWriter(); // Get writer from 'this.port'
|
||||
await writer.write(encoder.encode(command + '\r\n'));
|
||||
try {
|
||||
writer.releaseLock();
|
||||
} catch (err) {
|
||||
console.error('Ignoring release lock error', err);
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
this.controller.abort();
|
||||
await delay(50);
|
||||
await this.port.close();
|
||||
}
|
||||
|
||||
async reset() {
|
||||
console.debug('Triggering reset');
|
||||
await this.port.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: true,
|
||||
});
|
||||
await delay(250);
|
||||
await this.port.setSignals({
|
||||
dataTerminalReady: false,
|
||||
requestToSend: false,
|
||||
});
|
||||
|
||||
await delay(1250);
|
||||
}
|
||||
}
|
||||
435
lib/dfu.js
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
import * as zip from "./zip.min.js";
|
||||
|
||||
// Constants adapted from dfu/dfu_transport_serial.py
|
||||
const DFU_TOUCH_BAUD = 1200;
|
||||
const SERIAL_PORT_OPEN_WAIT_TIME = 0.1;
|
||||
const TOUCH_RESET_WAIT_TIME = 1.5;
|
||||
|
||||
const DEFAULT_SERIAL_PORT_TIMEOUT = 1.0; // Timeout time on serial port read
|
||||
const FLASH_PAGE_SIZE = 4096;
|
||||
const FLASH_PAGE_ERASE_TIME = 0.0897; // nRF52840 max erase time
|
||||
const FLASH_WORD_WRITE_TIME = 0.000100; // nRF52840 max write time
|
||||
const FLASH_PAGE_WRITE_TIME = (FLASH_PAGE_SIZE / 4) * FLASH_WORD_WRITE_TIME;
|
||||
const DFU_PACKET_MAX_SIZE = 512;
|
||||
|
||||
const DATA_INTEGRITY_CHECK_PRESENT = 1;
|
||||
const RELIABLE_PACKET = 1;
|
||||
const HCI_PACKET_TYPE = 14;
|
||||
|
||||
const DFU_INIT_PACKET = 1;
|
||||
const DFU_START_PACKET = 3;
|
||||
const DFU_DATA_PACKET = 4;
|
||||
const DFU_STOP_DATA_PACKET = 5;
|
||||
const DFU_ERASE_PAGE = 6; // Added for explicit page erase
|
||||
|
||||
const DFU_UPDATE_MODE_APP = 4;
|
||||
|
||||
// --- Utility Functions (adapted from dfu/util.py) ---
|
||||
|
||||
function int32ToBytes(value) {
|
||||
const buffer = new ArrayBuffer(4);
|
||||
const view = new DataView(buffer);
|
||||
view.setUint32(0, value, true); // Little-endian
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
function int16ToBytes(value) {
|
||||
const buffer = new ArrayBuffer(2);
|
||||
const view = new DataView(buffer);
|
||||
view.setUint16(0, value, true); // Little-endian
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
function slipPartsToFourBytes(seq, dip, rp, pktType, pktLen) {
|
||||
const ints = new Uint8Array(4);
|
||||
ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7);
|
||||
ints[1] = pktType | ((pktLen & 0x000F) << 4);
|
||||
ints[2] = (pktLen & 0x0FF0) >> 4;
|
||||
ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF;
|
||||
return ints;
|
||||
}
|
||||
|
||||
function slipEncodeEscChars(data) {
|
||||
const result = [];
|
||||
for (const byte of data) {
|
||||
if (byte === 0xC0) {
|
||||
result.push(0xDB, 0xDC);
|
||||
} else if (byte === 0xDB) {
|
||||
result.push(0xDB, 0xDD);
|
||||
} else {
|
||||
result.push(byte);
|
||||
}
|
||||
}
|
||||
return new Uint8Array(result);
|
||||
}
|
||||
|
||||
// --- CRC16 Calculation (adapted from dfu/crc16.py) ---
|
||||
|
||||
function calcCrc16(data, crc = 0xFFFF) {
|
||||
if (!(data instanceof Uint8Array)) {
|
||||
throw new Error("calcCrc16 requires Uint8Array input");
|
||||
}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
crc = ((crc >> 8) & 0x00FF) | ((crc << 8) & 0xFF00);
|
||||
crc ^= data[i];
|
||||
crc ^= (crc & 0x00FF) >> 4;
|
||||
crc ^= (crc << 8) << 4;
|
||||
crc ^= ((crc & 0x00FF) << 4) << 1;
|
||||
}
|
||||
return crc & 0xFFFF;
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
// --- HciPacket Class (adapted from dfu/dfu_transport_serial.py) ---
|
||||
|
||||
class HciPacket {
|
||||
static sequenceNumber = 0;
|
||||
|
||||
constructor(data) {
|
||||
HciPacket.sequenceNumber = (HciPacket.sequenceNumber + 1) % 8;
|
||||
let tempData = [];
|
||||
|
||||
const slipBytes = slipPartsToFourBytes(
|
||||
HciPacket.sequenceNumber,
|
||||
DATA_INTEGRITY_CHECK_PRESENT,
|
||||
RELIABLE_PACKET,
|
||||
HCI_PACKET_TYPE,
|
||||
data.length
|
||||
);
|
||||
tempData = tempData.concat(Array.from(slipBytes));
|
||||
|
||||
tempData = tempData.concat(Array.from(data));
|
||||
|
||||
// Add CRC
|
||||
const crc = calcCrc16(new Uint8Array(tempData));
|
||||
tempData.push(crc & 0xFF);
|
||||
tempData.push((crc & 0xFF00) >> 8);
|
||||
|
||||
const encoded = slipEncodeEscChars(new Uint8Array(tempData));
|
||||
this.data = new Uint8Array([0xC0, ...encoded, 0xC0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Main DFU Class ---
|
||||
|
||||
export class Dfu {
|
||||
/**
|
||||
* @param {SerialPort} port - The Web Serial API port object.
|
||||
* @param {boolean} [eraseBeforeUpdate=false] - Whether to erase the entire flash before updating.
|
||||
*/
|
||||
constructor(port, eraseBeforeUpdate = false) {
|
||||
this.port = port;
|
||||
this.transferInProgress = false;
|
||||
this.lastAck = -1;
|
||||
this.eraseBeforeUpdate = eraseBeforeUpdate; // Store the erase flag
|
||||
}
|
||||
|
||||
async sendPacket(pkt) {
|
||||
if (!this.port || !this.port.writable) {
|
||||
throw new Error("Serial port not open or not writable.");
|
||||
}
|
||||
|
||||
const writer = this.port.writable.getWriter();
|
||||
try {
|
||||
await writer.write(pkt.data);
|
||||
console.debug("Sent packet:", pkt.data);
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
|
||||
await this.getAck(); // Wait for ACK after sending
|
||||
}
|
||||
|
||||
async getAck() {
|
||||
if (!this.port || !this.port.readable) {
|
||||
throw new Error("Serial port not open or not readable.");
|
||||
}
|
||||
|
||||
const reader = this.port.readable.getReader();
|
||||
let buffer = [];
|
||||
let c0Count = 0;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
while (c0Count < 2) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
throw new Error("Stream closed before receiving full ACK.");
|
||||
}
|
||||
|
||||
if (value) {
|
||||
for (const byte of value) {
|
||||
buffer.push(byte);
|
||||
if (byte === 0xC0) {
|
||||
c0Count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > DEFAULT_SERIAL_PORT_TIMEOUT * 1000 * 5) { // Increased timeout for safety
|
||||
HciPacket.sequenceNumber = 0; // Reset sequence number on timeout.
|
||||
throw new Error("Timeout waiting for ACK.");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
// Decode SLIP
|
||||
const decodedData = this.decodeSlip(buffer);
|
||||
|
||||
// Extract ACK number (assuming it's in the decoded data)
|
||||
if (decodedData.length < 2) { // Check for sufficient length
|
||||
throw new Error("Received incomplete ACK.");
|
||||
}
|
||||
const ack = (decodedData[0] >> 3) & 0x07;
|
||||
|
||||
// Check for valid ACK sequence
|
||||
if (this.lastAck !== -1 && ack !== (this.lastAck + 1) % 8) {
|
||||
HciPacket.sequenceNumber = 0; // Reset on bad ack
|
||||
throw new Error(`Invalid ACK sequence. Expected ${(this.lastAck + 1) % 8}, got ${ack}`);
|
||||
}
|
||||
this.lastAck = ack;
|
||||
|
||||
return ack;
|
||||
}
|
||||
|
||||
decodeSlip(data) {
|
||||
const result = [];
|
||||
let i = 0;
|
||||
while (i < data.length) {
|
||||
if (data[i] === 0xDB) {
|
||||
i++;
|
||||
if (i >= data.length) {
|
||||
throw new Error("Invalid SLIP escape sequence: incomplete.");
|
||||
}
|
||||
if (data[i] === 0xDC) {
|
||||
result.push(0xC0);
|
||||
} else if (data[i] === 0xDD) {
|
||||
result.push(0xDB);
|
||||
} else {
|
||||
throw new Error(`Invalid SLIP escape sequence: DB followed by ${data[i].toString(16)}`);
|
||||
}
|
||||
} else if (data[i] === 0xC0) {
|
||||
// Ignore 0xC0 (start/end of packet)
|
||||
}
|
||||
else {
|
||||
result.push(data[i]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return new Uint8Array(result);
|
||||
}
|
||||
|
||||
async sendInitPacket(initPacket) {
|
||||
const frame = new Uint8Array([
|
||||
...int32ToBytes(DFU_INIT_PACKET),
|
||||
...initPacket,
|
||||
...int16ToBytes(0x0000), // Padding
|
||||
]);
|
||||
const packet = new HciPacket(frame);
|
||||
await this.sendPacket(packet);
|
||||
}
|
||||
|
||||
// THANKS Liam!!!
|
||||
static async forceDfuMode(port) {
|
||||
// open port
|
||||
await port.open({
|
||||
baudRate: DFU_TOUCH_BAUD,
|
||||
});
|
||||
|
||||
// wait SERIAL_PORT_OPEN_WAIT_TIME before closing port
|
||||
await sleep(SERIAL_PORT_OPEN_WAIT_TIME * 1000);
|
||||
|
||||
// close port
|
||||
await port.close();
|
||||
|
||||
// wait TOUCH_RESET_WAIT_TIME for device to enter into DFU mode
|
||||
await sleep(TOUCH_RESET_WAIT_TIME * 1000);
|
||||
}
|
||||
|
||||
async sendStartDfu(mode, softdeviceSize = 0, bootloaderSize = 0, appSize = 0) {
|
||||
const frame = new Uint8Array([
|
||||
...int32ToBytes(DFU_START_PACKET),
|
||||
...int32ToBytes(mode),
|
||||
...int32ToBytes(softdeviceSize),
|
||||
...int32ToBytes(bootloaderSize),
|
||||
...int32ToBytes(appSize),
|
||||
]);
|
||||
|
||||
const packet = new HciPacket(frame);
|
||||
await this.sendPacket(packet);
|
||||
|
||||
// Calculate and apply erase wait time.
|
||||
const totalSize = softdeviceSize + bootloaderSize + appSize;
|
||||
const eraseWaitTime = Math.max(0.5, ((totalSize / FLASH_PAGE_SIZE) + 1) * FLASH_PAGE_ERASE_TIME);
|
||||
await sleep(eraseWaitTime * 1000);
|
||||
}
|
||||
|
||||
|
||||
async sendErasePage(pageAddress) {
|
||||
const frame = new Uint8Array([
|
||||
...int32ToBytes(DFU_ERASE_PAGE),
|
||||
...int32ToBytes(pageAddress),
|
||||
]);
|
||||
const packet = new HciPacket(frame);
|
||||
await this.sendPacket(packet);
|
||||
await sleep(FLASH_PAGE_ERASE_TIME * 1000); // Wait for page erase
|
||||
}
|
||||
|
||||
|
||||
async eraseFlash(appSize) {
|
||||
console.log("Erasing flash...");
|
||||
const numPages = Math.ceil(appSize / FLASH_PAGE_SIZE);
|
||||
|
||||
// Assuming application starts at address 0x00000000
|
||||
let startAddress = 0x00000000;
|
||||
|
||||
for (let i = 0; i < numPages; i++) {
|
||||
const pageAddress = startAddress + (i * FLASH_PAGE_SIZE);
|
||||
console.log(`Erasing page ${i} at address 0x${pageAddress.toString(16)}`);
|
||||
await this.sendErasePage(pageAddress);
|
||||
}
|
||||
console.log("Flash erase complete.");
|
||||
}
|
||||
|
||||
|
||||
async sendFirmware(firmware, progressCallback) {
|
||||
const frames = [];
|
||||
let totalBytes = firmware.length;
|
||||
|
||||
// Chunk firmware into DFU packets
|
||||
for (let i = 0; i < firmware.length; i += DFU_PACKET_MAX_SIZE) {
|
||||
const chunk = firmware.subarray(i, i + DFU_PACKET_MAX_SIZE);
|
||||
const frame = new Uint8Array([
|
||||
...int32ToBytes(DFU_DATA_PACKET),
|
||||
...chunk,
|
||||
]);
|
||||
const dataPacket = new HciPacket(frame);
|
||||
frames.push(dataPacket);
|
||||
}
|
||||
|
||||
let bytesSent = 0;
|
||||
// Send firmware packets
|
||||
for (const [index, pkt] of frames.entries()) {
|
||||
await this.sendPacket(pkt);
|
||||
bytesSent += pkt.data.length - 6; // Correctly calculate sent bytes, excluding SLIP overhead
|
||||
|
||||
if (progressCallback) {
|
||||
const progress = Math.min(100, Math.round((bytesSent / totalBytes) * 100)); // Ensure progress doesn't exceed 100
|
||||
progressCallback(progress);
|
||||
}
|
||||
|
||||
// Wait after every 8 frames (one flash page)
|
||||
if ((index + 1) % 8 === 0) {
|
||||
await sleep(FLASH_PAGE_WRITE_TIME * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the last page to be written
|
||||
await sleep(FLASH_PAGE_WRITE_TIME * 1000);
|
||||
|
||||
// Send stop packet
|
||||
const stopPacket = new HciPacket(int32ToBytes(DFU_STOP_DATA_PACKET));
|
||||
await this.sendPacket(stopPacket);
|
||||
}
|
||||
|
||||
async dfuUpdate(zipFile, progressCallback) {
|
||||
if (this.transferInProgress) {
|
||||
throw new Error("DFU update already in progress.");
|
||||
}
|
||||
this.transferInProgress = true;
|
||||
this.lastAck = -1; // Reset last ACK
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
await this.port.open({ baudRate: 115200 }); // Open with correct baudrate
|
||||
|
||||
const reader = new zip.ZipReader(new zip.BlobReader(zipFile));
|
||||
const entries = await reader.getEntries();
|
||||
|
||||
let manifest = null;
|
||||
let firmwareFiles = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
const filename = decoder.decode(entry.rawFilename);
|
||||
console.debug('Found zip filename: ', filename);
|
||||
if (filename === 'manifest.json') {
|
||||
const text = await entry.getData(new zip.TextWriter());
|
||||
manifest = JSON.parse(text);
|
||||
} else if (filename.endsWith('.bin') || filename.endsWith('.dat')) {
|
||||
firmwareFiles[filename] = await entry.getData(new zip.Uint8ArrayWriter());
|
||||
}
|
||||
}
|
||||
|
||||
await reader.close();
|
||||
|
||||
if (!manifest) {
|
||||
throw new Error("manifest.json not found in the ZIP file.");
|
||||
}
|
||||
if (!firmwareFiles[manifest.manifest.application.bin_file] ||
|
||||
!firmwareFiles[manifest.manifest.application.dat_file])
|
||||
{
|
||||
throw new Error("Application .bin or .dat file not found.");
|
||||
}
|
||||
|
||||
const appBin = firmwareFiles[manifest.manifest.application.bin_file];
|
||||
const initPacket = firmwareFiles[manifest.manifest.application.dat_file];
|
||||
const appSize = appBin.length;
|
||||
|
||||
// Erase flash if requested
|
||||
if (this.eraseBeforeUpdate) {
|
||||
await this.eraseFlash(appSize);
|
||||
}
|
||||
|
||||
// Start DFU
|
||||
await this.sendStartDfu(DFU_UPDATE_MODE_APP, 0, 0, appSize);
|
||||
|
||||
// Send Init Packet
|
||||
await this.sendInitPacket(initPacket);
|
||||
|
||||
// Send Firmware
|
||||
await this.sendFirmware(appBin, progressCallback);
|
||||
|
||||
console.log("DFU update complete.");
|
||||
|
||||
} catch (error) {
|
||||
console.error("DFU Update failed:", error);
|
||||
throw error; // Re-throw the error for handling by the caller
|
||||
} finally {
|
||||
this.transferInProgress = false;
|
||||
if (this.port && this.port.readable) {
|
||||
try {
|
||||
const reader = this.port.readable.getReader();
|
||||
await reader.cancel();
|
||||
reader.releaseLock();
|
||||
|
||||
} catch (error) {
|
||||
// Ignore errors when trying to cancel the reader
|
||||
console.debug(`Error: closing reader: ${error}`);
|
||||
}
|
||||
}
|
||||
if (this.port && this.port.writable) {
|
||||
try {
|
||||
const writer = this.port.writable.getWriter();
|
||||
await writer.close();
|
||||
writer.releaseLock();
|
||||
} catch(error) {
|
||||
// Ignore errors when trying to close the writer
|
||||
console.debug(`Error: closing writer: ${error}`);
|
||||
}
|
||||
}
|
||||
if (this.port) {
|
||||
try {
|
||||
await this.port.close();
|
||||
}
|
||||
catch (error) {
|
||||
// Ignore errors when trying to close the port
|
||||
console.debug(`Error: closing port: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||