cosmetic change

This commit is contained in:
Rastislav Vysoky 2025-02-23 10:27:17 +01:00
parent de525c90d5
commit 92805db7ae
4 changed files with 1125 additions and 1077 deletions

View file

@ -1,375 +1,375 @@
{ {
"basePath": "./firmware", "basePath": "./firmware",
"role": { "role": {
"gui": { "gui": {
"icon": "gradient", "icon": "gradient",
"title": "Client GUI", "title": "Client GUI",
"tooltip": "all your device settings are saved on internal flash" "tooltip": "all your device settings are saved on internal flash"
}, },
"guiSD": { "guiSD": {
"icon": "gradient", "icon": "gradient",
"title": "Client GUI: data on SD card", "title": "Client GUI: data on SD card",
"tooltip": "all your device settings are saved on SD card" "tooltip": "all your device settings are saved on SD card"
}, },
"companionBle": { "companionBle": {
"icon": "smartphone", "icon": "smartphone",
"class": "primary-text", "class": "primary-text",
"title": "Companion radio: Bluetooth", "title": "Companion radio: Bluetooth",
"tooltip": "Chat via mobile phone App or Web Client" "tooltip": "Chat via mobile phone App or Web Client"
}, },
"companionUsb": { "companionUsb": {
"icon": "usb", "icon": "usb",
"title": "Companion radio: USB", "title": "Companion radio: USB",
"tooltip": "Chat via Web client or command line client" "tooltip": "Chat via Web client or command line client"
}, },
"repeater": { "repeater": {
"icon": "cell_tower", "icon": "cell_tower",
"title": "Repeater", "title": "Repeater",
"tooltip": "Special role just for routing packets. Configured via <b>Console</b> on flasher main page" "tooltip": "Special role just for routing packets. Configured via <b>Console</b> on flasher main page"
}, },
"roomServer": { "roomServer": {
"icon": "forum", "icon": "forum",
"title": "Room Server", "title": "Room Server",
"tooltip": "Special role for local room and routing packets. Configured via <b>Console</b> on flasher main page" "tooltip": "Special role for local room and routing packets. Configured via <b>Console</b> on flasher main page"
} }
}, },
"device": [ "device": [
{ {
"name": "Lilygo T-Deck", "name": "Lilygo T-Deck",
"tooltip": "<img class='device' src='/img/lilygo_tdeck.png'>", "tooltip": "<img class='device' src='/img/lilygo_tdeck.png'>",
"type": "esp32", "type": "esp32",
"firmware": [ "firmware": [
{ {
"role": "gui", "role": "gui",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "RippleUltra-TDeck-v6.0-beta21-merged.bin", "name": "RippleUltra-TDeck-v6.0-beta21-merged.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
}, },
{ {
"type": "download", "type": "download",
"name": "RippleUltra-TDeck-v6.0-beta21.bin", "name": "RippleUltra-TDeck-v6.0-beta21.bin",
"title": "App firmware bin (use with m5 booloader)" "title": "App firmware bin (use with m5 booloader)"
} }
] ]
}, },
{ {
"role": "guiSD", "role": "guiSD",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "RippleUltra-TDeck-SD-v6.0-beta21-merged.bin", "name": "RippleUltra-TDeck-SD-v6.0-beta21-merged.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
}, },
{ {
"type": "download", "type": "download",
"name": "RippleUltra-TDeck-SD-v6.0-beta21.bin", "name": "RippleUltra-TDeck-SD-v6.0-beta21.bin",
"title": "App firmware bin (use with m5 booloader)" "title": "App firmware bin (use with m5 booloader)"
} }
] ]
} }
] ]
}, },
{ {
"name": "Lilygo T3 S3", "name": "Lilygo T3 S3",
"type": "esp32", "type": "esp32",
"tooltip": "<img class='device' src='/img/lilygo_t3s3.png'>", "tooltip": "<img class='device' src='/img/lilygo_t3s3.png'>",
"firmware": [ "firmware": [
{ {
"role": "companionUsb", "role": "companionUsb",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "LilyGo_T3S3_sx1262_companion_radio_usb.bin", "name": "LilyGo_T3S3_sx1262_companion_radio_usb.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "companionBle", "role": "companionBle",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "LilyGo_T3S3_sx1262_companion_radio_ble.bin", "name": "LilyGo_T3S3_sx1262_companion_radio_ble.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "LilyGo_T3S3_sx1262_Repeater.bin", "name": "LilyGo_T3S3_sx1262_Repeater.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
} }
] ]
}, },
{ {
"name": "Heltec v2", "name": "Heltec v2",
"type": "esp32", "type": "esp32",
"tooltip": "<img class='device' src='/img/heltec_v3.png'>", "tooltip": "<img class='device' src='/img/heltec_v3.png'>",
"firmware": [ "firmware": [
{ {
"role": "companionUsb", "role": "companionUsb",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_v2_companion_radio_usb.bin", "name": "Heltec_v2_companion_radio_usb.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_v2_repeater.bin", "name": "Heltec_v2_repeater.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
} }
] ]
}, },
{ {
"name": "Heltec v3", "name": "Heltec v3",
"type": "esp32", "type": "esp32",
"tooltip": "<img class='device' src='/img/heltec_v3.png'>", "tooltip": "<img class='device' src='/img/heltec_v3.png'>",
"firmware": [ "firmware": [
{ {
"role": "companionUsb", "role": "companionUsb",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_v3_companion_radio_usb.bin", "name": "Heltec_v3_companion_radio_usb.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "companionBle", "role": "companionBle",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_v3_companion_radio_ble.bin", "name": "Heltec_v3_companion_radio_ble.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_v3_repeater.bin", "name": "Heltec_v3_repeater.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "roomServer", "role": "roomServer",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_v3_room_server.bin", "name": "Heltec_v3_room_server.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
} }
] ]
}, },
{ {
"name": "Heltec T114", "name": "Heltec T114",
"type": "nrf52", "type": "nrf52",
"tooltip": "<img class='device' src='/img/heltec_t114.png'>", "tooltip": "<img class='device' src='/img/heltec_t114.png'>",
"firmware": [ "firmware": [
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_T114_repeater.zip", "name": "Heltec_T114_repeater.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "Heltec_T114_repeater.uf2", "name": "Heltec_T114_repeater.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
}, },
{ {
"role": "roomServer", "role": "roomServer",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Heltec_T114_room_server.zip", "name": "Heltec_T114_room_server.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "Heltec_T114_room_server.uf2", "name": "Heltec_T114_room_server.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
} }
] ]
}, },
{ {
"name": "RAK Wireless WisBlock / WisMesh (RAK 4631)", "name": "RAK Wireless WisBlock / WisMesh (RAK 4631)",
"type": "nrf52", "type": "nrf52",
"tooltip": "<img class='device' src='/img/rak_4631.png'>", "tooltip": "<img class='device' src='/img/rak_4631.png'>",
"firmware": [ "firmware": [
{ {
"role": "companionUsb", "role": "companionUsb",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "RAK_4631_companion_radio_usb.zip", "name": "RAK_4631_companion_radio_usb.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "RAK_4631_companion_radio_usb.uf2", "name": "RAK_4631_companion_radio_usb.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
}, },
{ {
"role": "companionBle", "role": "companionBle",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "RAK_4631_companion_radio_ble.zip", "name": "RAK_4631_companion_radio_ble.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "RAK_4631_companion_radio_ble.uf2", "name": "RAK_4631_companion_radio_ble.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
}, },
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "RAK_4631_Repeater.zip", "name": "RAK_4631_Repeater.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "RAK_4631_Repeater.uf2", "name": "RAK_4631_Repeater.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
}, },
{ {
"role": "roomServer", "role": "roomServer",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "RAK_4631_room_server.zip", "name": "RAK_4631_room_server.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "RAK_4631_room_server.uf2", "name": "RAK_4631_room_server.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
} }
] ]
}, },
{ {
"name": "Seeed Studio SenseCAP T1000-E", "name": "Seeed Studio SenseCAP T1000-E",
"tooltip": "<img class='device' src='/img/seeed_t1000e.png'>", "tooltip": "<img class='device' src='/img/seeed_t1000e.png'>",
"type": "nrf52", "type": "nrf52",
"firmware": [ "firmware": [
{ {
"role": "companionBle", "role": "companionBle",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Seeed_T1000e_companion_radio_ble.zip", "name": "Seeed_T1000e_companion_radio_ble.zip",
"title": "firmware OTA zip" "title": "firmware OTA zip"
}, },
{ {
"type": "download", "type": "download",
"name": "Seeed_T1000e_companion_radio_ble.uf2", "name": "Seeed_T1000e_companion_radio_ble.uf2",
"title": "firmware uf2" "title": "firmware uf2"
} }
] ]
} }
] ]
}, },
{ {
"name": "Seeed Studio Xiao C3", "name": "Seeed Studio Xiao C3",
"tooltip": "<img class='device' src='/img/xiao_s3.png'>", "tooltip": "<img class='device' src='/img/xiao_s3.png'>",
"type": "esp32", "type": "esp32",
"firmware": [ "firmware": [
{ {
"role": "repeater", "role": "repeater",
"title": "Repeater (Semtech SX1262)", "title": "Repeater (Semtech SX1262)",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Xiao_C3_Repeater_sx1262.bin", "name": "Xiao_C3_Repeater_sx1262.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
}, },
{ {
"role": "repeater", "role": "repeater",
"title": "Repeater (Semtech SX1268)", "title": "Repeater (Semtech SX1268)",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Xiao_C3_Repeater_sx1268.bin", "name": "Xiao_C3_Repeater_sx1268.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
} }
] ]
}, },
{ {
"name": "Seeed Studio Xiao S3 WIO", "name": "Seeed Studio Xiao S3 WIO",
"tooltip": "<img class='device' src='/img/xiao_s3.png'>", "tooltip": "<img class='device' src='/img/xiao_s3.png'>",
"type": "esp32", "type": "esp32",
"firmware": [ "firmware": [
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Xiao_S3_WIO_Repeater.bin", "name": "Xiao_S3_WIO_Repeater.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
} }
] ]
}, },
{ {
"name": "UnitEng Station G2", "name": "UnitEng Station G2",
"tooltip": "<img class='device' src='/img/station_g2.png'>", "tooltip": "<img class='device' src='/img/station_g2.png'>",
"type": "esp32", "type": "esp32",
"firmware": [ "firmware": [
{ {
"role": "repeater", "role": "repeater",
"files": [ "files": [
{ {
"type": "flash", "type": "flash",
"name": "Station_G2_repeater.bin", "name": "Station_G2_repeater.bin",
"title": "Combined app+partition+bootloader firmware bin" "title": "Combined app+partition+bootloader firmware bin"
} }
] ]
} }
] ]
} }
] ]
} }

View file

@ -0,0 +1,48 @@
body {
display: flex;
height: 100vh;
}
#flasher {
flex-grow: 1;
}
#flasher img.device {
width: 300px;
}
#flasher div.autoscroller {
overflow: auto;
max-height: 300px;
display: flex;
flex-direction: column-reverse;
}
#flasher pre.term {
font-family: monospace;
}
#flasher .overlay {
display: flex !important;
flex-direction: column;
}
#flasher .console {
overflow: auto;
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 0;
}
#flasher .console .holder {
display: flex;
flex-direction: row;
gap: 10px;
}
#flasher .console-input {
flex-grow: 1;
display: block;
appearance: none;
background: transparent;
border: 0;
font-family: monospace;
font-size: .875rem;
}
#flasher .console-input:focus, #flasher console-input:focus{
outline: none;
}

View file

@ -7,390 +7,390 @@ import { SerialConsole } from './lib/console.js';
const res = await fetch('./config.json'); const res = await fetch('./config.json');
const config = await res.json(); const config = await res.json();
const commandReference = { const commandReference = {
'set freq ': 'Set frequency {Mhz}', 'set freq ': 'Set frequency {Mhz}',
'time ': 'Set time {epoch-secs}', 'time ': 'Set time {epoch-secs}',
'erase': 'Erase filesystem', 'erase': 'Erase filesystem',
'advert': 'Send Advertisment packet', 'advert': 'Send Advertisment packet',
'reboot': 'Reboot device', 'reboot': 'Reboot device',
'clock': 'Display current time', 'clock': 'Display current time',
'password ': 'Set new password', 'password ': 'Set new password',
'log': 'Ouput log', 'log': 'Ouput log',
'log start': 'Start packet logging to file system', 'log start': 'Start packet logging to file system',
'log stop': 'Stop packet logging to file system', 'log stop': 'Stop packet logging to file system',
'log erase': 'Erase the packet logs from file system', 'log erase': 'Erase the packet logs from file system',
'ver': 'Show device version', 'ver': 'Show device version',
'set af ': 'Set Air-time factor', 'set af ': 'Set Air-time factor',
'set tx ': 'Set Tx power {dBm}', 'set tx ': 'Set Tx power {dBm}',
'set repeat ': 'Set repeater mode {on|off}', 'set repeat ': 'Set repeater mode {on|off}',
'set advert.interval ': 'Set advert rebroadcast interval {minutes}', 'set advert.interval ': 'Set advert rebroadcast interval {minutes}',
'set guest.password ': 'Set guest password', 'set guest.password ': 'Set guest password',
'set name ': 'Set advertisement name', 'set name ': 'Set advertisement name',
'set lat': 'Set the advertisement map latitude', 'set lat': 'Set the advertisement map latitude',
'set lon': 'Set the advertisement map longitude', 'set lon': 'Set the advertisement map longitude',
}; };
function setup() { function setup() {
const consoleEditBox = ref(); const consoleEditBox = ref();
const consoleWindow = ref(); const consoleWindow = ref();
const selected = reactive({ const selected = reactive({
device: null, device: null,
firmware: null, firmware: null,
wipe: false, wipe: false,
port: null port: null
}); });
const flashing = reactive({ const flashing = reactive({
instance: null, instance: null,
active: false, active: false,
percentage: 0, percentage: 0,
log: '', log: '',
error: '', error: '',
dfuComplete: false, dfuComplete: false,
}); });
const serialCon = reactive({ const serialCon = reactive({
instance: null, instance: null,
opened: false, opened: false,
content: '', content: '',
edit: '', edit: '',
}); });
window.app = { selected, flashing, serialCon }; window.app = { selected, flashing, serialCon };
const log = { const log = {
clean() { flashing.log = '' }, clean() { flashing.log = '' },
write(data) { flashing.log += data }, write(data) { flashing.log += data },
writeLine(data) { flashing.log += data + '\n' } writeLine(data) { flashing.log += data + '\n' }
}; };
const refresh = () => { const refresh = () => {
location.reload(); location.reload();
} }
const flasherCleanup = async () => { const flasherCleanup = async () => {
const port = selected.port; const port = selected.port;
flashing.active = false; flashing.active = false;
flashing.log = ''; flashing.log = '';
flashing.error = ''; flashing.error = '';
flashing.dfuComplete = false; flashing.dfuComplete = false;
flashing.percentage = 0; flashing.percentage = 0;
selected.firmware = null; selected.firmware = null;
selected.wipe = false; selected.wipe = false;
selected.device = null; selected.device = null;
if(flashing.instance instanceof ESPLoader) { if(flashing.instance instanceof ESPLoader) {
await flashing.instance?.hr.reset(); await flashing.instance?.hr.reset();
await flashing.instance?.transport?.disconnect(); await flashing.instance?.transport?.disconnect();
} }
flashing.instance = null; flashing.instance = null;
} }
const openSerialCon = async() => { const openSerialCon = async() => {
const port = selected.port = await navigator.serial.requestPort(); const port = selected.port = await navigator.serial.requestPort();
const serialConsole = serialCon.instance = new SerialConsole(port); const serialConsole = serialCon.instance = new SerialConsole(port);
serialCon.content = 'Welcome to MeshCore serial console.\n'; serialCon.content = 'Welcome to MeshCore serial console.\n';
serialCon.content += 'If you came here right after flashing, please restart your device.\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'; serialCon.content += 'Click on the cursor to get all supported commands.\n\n';
serialConsole.onOutput = (text) => { serialConsole.onOutput = (text) => {
serialCon.content += text; serialCon.content += text;
}; };
serialConsole.connect(); serialConsole.connect();
serialCon.opened = true; serialCon.opened = true;
await nextTick(); await nextTick();
consoleEditBox.value.focus(); consoleEditBox.value.focus();
} }
const closeSerialCon = async() => { const closeSerialCon = async() => {
serialCon.opened = false; serialCon.opened = false;
await serialCon.instance.disconnect(); await serialCon.instance.disconnect();
} }
const sendCommand = async(text) => { const sendCommand = async(text) => {
const consoleEl = consoleWindow.value; const consoleEl = consoleWindow.value;
serialCon.edit = ''; serialCon.edit = '';
await serialCon.instance.sendCommand(text); await serialCon.instance.sendCommand(text);
setTimeout(() => consoleEl.scrollTop = consoleEl.scrollHeight, 100); setTimeout(() => consoleEl.scrollTop = consoleEl.scrollHeight, 100);
} }
const dfuMode = async() => { const dfuMode = async() => {
await Dfu.forceDfuMode(await navigator.serial.requestPort({})) await Dfu.forceDfuMode(await navigator.serial.requestPort({}))
flashing.dfuComplete = true; flashing.dfuComplete = true;
} }
const flashDevice = async() => { const flashDevice = async() => {
const device = selected.device; const device = selected.device;
const firmware = selected.firmware; const firmware = selected.firmware;
const flashFile = firmware.files.find(f => f.type === 'flash'); const flashFile = firmware.files.find(f => f.type === 'flash');
if(!flashFile) { if(!flashFile) {
alert('Cannot find configuration for flash file! please report this to Discord.') alert('Cannot find configuration for flash file! please report this to Discord.')
flasherCleanup(); flasherCleanup();
return; return;
} }
const url = `${config.basePath}/${flashFile.name}`; const url = `${config.basePath}/${flashFile.name}`;
const resp = await fetch(url); const resp = await fetch(url);
const port = selected.port = await navigator.serial.requestPort({}); const port = selected.port = await navigator.serial.requestPort({});
if(device.type === 'esp32') { if(device.type === 'esp32') {
let esploader; let esploader;
let fileData; let fileData;
let transport; let transport;
try { try {
const reader = new FileReader(); const reader = new FileReader();
fileData = await new Promise(async (resolve) => { fileData = await new Promise(async (resolve) => {
reader.addEventListener('load', () => resolve(reader.result)); reader.addEventListener('load', () => resolve(reader.result));
reader.readAsBinaryString(await resp.blob()); reader.readAsBinaryString(await resp.blob());
}); });
} }
catch(e) { catch(e) {
console.error(e); console.error(e);
flashing.error = `Cannot read flash file: ${e}`; flashing.error = `Cannot read flash file: ${e}`;
return; return;
} }
const flashOptions = { const flashOptions = {
terminal: log, terminal: log,
compress: true, compress: true,
eraseAll: selected.wipe, eraseAll: selected.wipe,
flashSize: 'keep', flashSize: 'keep',
flashMode: 'keep', flashMode: 'keep',
flashFreq: 'keep', flashFreq: 'keep',
baudrate: 115200, baudrate: 115200,
romBaudrate: 115200, romBaudrate: 115200,
enableTracing: false, enableTracing: false,
fileArray: [{ fileArray: [{
data: fileData, data: fileData,
address: 0 address: 0
}], }],
reportProgress: async (fileIndex, written, total) => { reportProgress: async (fileIndex, written, total) => {
flashing.percentage = (written / total) * 100; flashing.percentage = (written / total) * 100;
// we're done with this file // we're done with this file
if (written === total) { if (written === total) {
return; return;
} }
}, },
}; };
try { try {
flashing.active = true; flashing.active = true;
transport = new Transport(port, true); transport = new Transport(port, true);
flashOptions.transport = transport flashOptions.transport = transport
flashing.instance = esploader = new ESPLoader(flashOptions); flashing.instance = esploader = new ESPLoader(flashOptions);
esploader.hr = new HardReset(transport); esploader.hr = new HardReset(transport);
await esploader.main(); await esploader.main();
await esploader.flashId(); await esploader.flashId();
} }
catch(e) { catch(e) {
console.error(e); console.error(e);
flashing.error = `Failed to initialize. Did you place the device into firmware download mode? Detail: ${e}`; flashing.error = `Failed to initialize. Did you place the device into firmware download mode? Detail: ${e}`;
esploader = null; esploader = null;
return; return;
} }
try { try {
await esploader.writeFlash(flashOptions); await esploader.writeFlash(flashOptions);
await esploader.after(); await esploader.after();
} }
catch(e) { catch(e) {
console.error(e); console.error(e);
flashing.error = `ESP32 flashing failed: ${e}`; flashing.error = `ESP32 flashing failed: ${e}`;
await esploader.hardReset(); await esploader.hardReset();
await transport.disconnect(); await transport.disconnect();
return; return;
} }
} }
else if(device.type === 'nrf52') { else if(device.type === 'nrf52') {
const dfu = this.flashing.instance = new Dfu(port, selected.wipe); const dfu = this.flashing.instance = new Dfu(port, selected.wipe);
const zipFile = await resp.blob(); const zipFile = await resp.blob();
flashing.active = true; flashing.active = true;
try { try {
await dfu.dfuUpdate(zipFile, async (progress) => { await dfu.dfuUpdate(zipFile, async (progress) => {
flashing.percentage = progress; flashing.percentage = progress;
}); });
} }
catch(e) { catch(e) {
console.error(e); console.error(e);
flashing.error = `nRF flashing failed: ${e}`; flashing.error = `nRF flashing failed: ${e}`;
return; return;
} }
} }
}; };
return { return {
consoleEditBox, consoleWindow, consoleEditBox, consoleWindow,
config, selected, flashing, config, selected, flashing,
flashDevice, flasherCleanup, dfuMode, flashDevice, flasherCleanup, dfuMode,
serialCon, openSerialCon, sendCommand, closeSerialCon, serialCon, openSerialCon, sendCommand, closeSerialCon,
refresh, commandReference refresh, commandReference
} }
} }
const template = ` const template = `
<div class="flash-container"> <div class="flash-container">
<div v-if="flashing.active"> <div v-if="flashing.active">
<header> <header>
<nav> <nav>
<i>developer_board</i> <i>developer_board</i>
<span class="small">{{ selected.device.name }}</span> <span class="small">{{ selected.device.name }}</span>
<i>chevron_right</i> <i>chevron_right</i>
<i>{{ selected.firmware.icon }}</i> <i>{{ selected.firmware.icon }}</i>
<span class="small">{{ selected.firmware.title }}</span> <span class="small">{{ selected.firmware.title }}</span>
</nav> </nav>
</header> </header>
<article v-if="flashing.error"> <article v-if="flashing.error">
<div class="row"> <div class="row">
<div class="max"> <div class="max">
<h6>Flashing failed!</h6> <h6>Flashing failed!</h6>
<p><span>{{ flashing.error }}</span></p> <p><span>{{ flashing.error }}</span></p>
<p><button @click="refresh()">Retry</button></p> <p><button @click="refresh()">Retry</button></p>
</div> </div>
</div> </div>
</article> </article>
<article v-else> <article v-else>
<div class="row"> <div class="row">
<div class="max" v-if="flashing.percentage < 100"> <div class="max" v-if="flashing.percentage < 100">
<h6><progress class="circle small"></progress> Flashing...</h6> <h6><progress class="circle small"></progress> Flashing...</h6>
<p>Please do not disconnect the device</p> <p>Please do not disconnect the device</p>
</div> </div>
<div class="max" v-else=> <div class="max" v-else=>
<h6>Flashing complete!</h6> <h6>Flashing complete!</h6>
<p> <p>
<button @click="flasherCleanup()">Close</button> <button @click="flasherCleanup()">Close</button>
</p> </p>
</div> </div>
</div> </div>
<div class="autoscroller"> <div class="autoscroller">
<pre class="term" v-if="flashing.terminal">{{ flashing.terminal }}</pre> <pre class="term" v-if="flashing.terminal">{{ flashing.terminal }}</pre>
</div> </div>
<nav> <nav>
<progress :value="flashing.percentage" max="100"></progress> <progress :value="flashing.percentage" max="100"></progress>
</nav> </nav>
</article> </article>
</div> </div>
<div v-else-if="selected.firmware"> <div v-else-if="selected.firmware">
<header> <header>
<nav> <nav>
<button class="circle transparent" @click="selected.firmware = null"><i>arrow_back</i></button> <button class="circle transparent" @click="selected.firmware = null"><i>arrow_back</i></button>
<i>developer_board</i> <i>developer_board</i>
<a class="small" href="javascript:;" @click="selected.firmware = null">{{ selected.device.name }}</a> <a class="small" href="javascript:;" @click="selected.firmware = null">{{ selected.device.name }}</a>
<i>chevron_right</i> <i>chevron_right</i>
<i>{{ selected.firmware.icon }}</i> <i>{{ selected.firmware.icon }}</i>
<span class="small">{{ selected.firmware.desc }}</span> <span class="small">{{ selected.firmware.desc }}</span>
</nav> </nav>
<nav class="no-margin"> <nav class="no-margin">
<h6 class="small max">Install options</h6> <h6 class="small max">Install options</h6>
</nav> </nav>
</header> </header>
<ul class="list border" v-if="selected.device.type === 'esp32'"> <ul class="list border" v-if="selected.device.type === 'esp32'">
<li> <li>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="selected.wipe"> <input type="checkbox" v-model="selected.wipe">
<span>Erase device</span> <span>Erase device</span>
<div class="tooltip right max"> <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. 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> </div>
</label> </label>
</li> </li>
</ul> </ul>
<button @click="dfuMode" :disabled="flashing.dfuComplete" v-if="selected.device.type === 'nrf52'"> <button @click="dfuMode" :disabled="flashing.dfuComplete" v-if="selected.device.type === 'nrf52'">
<i>{{ flashing.dfuComplete ? 'check' : 'code' }}</i> <i>{{ flashing.dfuComplete ? 'check' : 'code' }}</i>
<span>{{ flashing.dfuComplete ? 'DFU mode active' : 'Enter DFU mode' }}</span> <span>{{ flashing.dfuComplete ? 'DFU mode active' : 'Enter DFU mode' }}</span>
<div class="tooltip right max"> <div class="tooltip right max">
Enter DFU mode - this mode enables you to flash your firmware. Enter DFU mode - this mode enables you to flash your firmware.
If you did not trigger the DFU mode manually, please click this button. If you did not trigger the DFU mode manually, please click this button.
</div> </div>
</button> </button>
<div class="medium-space"></div> <div class="medium-space"></div>
<nav class="small-margin"> <nav class="small-margin">
<button @click="flashDevice"> <button @click="flashDevice">
<i>bolt</i> <i>bolt</i>
<span>Flash!</span> <span>Flash!</span>
<div class="tooltip right max"> <div class="tooltip right max">
Upload the firmware into your device. Existing firwmare will get overwritten. 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> <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> </div>
</button> </button>
<div class="max"></div> <div class="max"></div>
<button data-ui="#down" class="active"> <button data-ui="#down" class="active">
<i>download</i> <i>download</i>
<span>Download</span><i>arrow_drop_down</i> <span>Download</span><i>arrow_drop_down</i>
<menu class="no-wrap" id="down" data-ui="#down"> <menu class="no-wrap" id="down" data-ui="#down">
<li v-for="file in selected.firmware.files"> <li v-for="file in selected.firmware.files">
<a data-ui="menu-selector" :href="config.basePath + '/' + file.name" download>{{ file.title }}</a> <a data-ui="menu-selector" :href="config.basePath + '/' + file.name" download>{{ file.title }}</a>
</li> </li>
</menu> </menu>
<div class="tooltip left max">Download a copy of the firmware files for use with other flashers</div> <div class="tooltip left max">Download a copy of the firmware files for use with other flashers</div>
</button> </button>
</nav> </nav>
</div> </div>
<div v-else-if="selected.device"> <div v-else-if="selected.device">
<header> <header>
<nav> <nav>
<button class="circle transparent" @click="selected.device = null"><i>arrow_back</i></button> <button class="circle transparent" @click="selected.device = null"><i>arrow_back</i></button>
<i>developer_board</i> <i>developer_board</i>
<span>{{ selected.device.name }}</span> <span>{{ selected.device.name }}</span>
</nav> </nav>
<nav class="no-margin"> <nav class="no-margin">
<h6 class="small max">Choose role</h6> <h6 class="small max">Choose role</h6>
</nav> </nav>
</header> </header>
<ul class="list border"> <ul class="list border">
<li v-for="firmware in selected.device.firmware" :class="firmware.class || config.role[firmware.role].class || ''"> <li v-for="firmware in selected.device.firmware" :class="firmware.class || config.role[firmware.role].class || ''">
<button class="transparent" @click="selected.firmware = firmware"> <button class="transparent" @click="selected.firmware = firmware">
<i>{{ firmware.icon || config.role[firmware.role].icon }}</i> <i>{{ firmware.icon || config.role[firmware.role].icon }}</i>
<span>{{ firmware.title || config.role[firmware.role].title }}</span> <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> <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> </button>
</li> </li>
</ul> </ul>
</div> </div>
<div v-else> <div v-else>
<header> <header>
<nav> <nav>
<i>bolt</i> <i>bolt</i>
<h5 class="small max">MeshCore flasher</h5> <h5 class="small max">MeshCore flasher</h5>
<button class="transparent" @click="openSerialCon()"> <button class="transparent" @click="openSerialCon()">
<i>terminal</i> <i>terminal</i>
<span>Console</span> <span>Console</span>
<div class="tooltip left max">Open serial console to manage Routers and Room servers via serial terminal</div> <div class="tooltip left max">Open serial console to manage Routers and Room servers via serial terminal</div>
</button> </button>
</nav> </nav>
<nav class="no-margin"> <nav class="no-margin">
<h6 class="small max">Choose device</h6> <h6 class="small max">Choose device</h6>
</nav> </nav>
</header> </header>
<ul class="list border"> <ul class="list border">
<li v-for="device in config.device"> <li v-for="device in config.device">
<button class="transparent" @click="selected.device = device"> <button class="transparent" @click="selected.device = device">
<i>developer_board</i> <i>developer_board</i>
<span>{{ device.name }}</span> <span>{{ device.name }}</span>
<div class="tooltip right max" v-if="device.tooltip" v-html="device.tooltip"></div> <div class="tooltip right max" v-if="device.tooltip" v-html="device.tooltip"></div>
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div v-if="serialCon.opened" class="overlay active"> <div v-if="serialCon.opened" class="overlay active">
<datalist id="command-db"> <datalist id="command-db">
<option v-for="(desc, command) in commandReference" :value="command">{{ desc }}</option> <option v-for="(desc, command) in commandReference" :value="command">{{ desc }}</option>
</datalist> </datalist>
<header> <header>
<nav> <nav>
<button class="circle transparent" @click="closeSerialCon()"><i>arrow_back</i></button> <button class="circle transparent" @click="closeSerialCon()"><i>arrow_back</i></button>
<h6 class="small max">Serial Console</h6> <h6 class="small max">Serial Console</h6>
</nav> </nav>
</header> </header>
<pre class="console" @click="consoleEditBox.focus()" ref="consoleWindow"> <pre class="console" @click="consoleEditBox.focus()" ref="consoleWindow">
<code>{{ serialCon.content }}</code> <code>{{ serialCon.content }}</code>
<div class="holder"> <div class="holder">
<span>&gt;</span> <span>&gt;</span>
<input ref="consoleEditBox" class="console-input" type="text" v-model="serialCon.edit" @keydown.enter.prevent="sendCommand(serialCon.edit)" list="command-db"> <input ref="consoleEditBox" class="console-input" type="text" v-model="serialCon.edit" @keydown.enter.prevent="sendCommand(serialCon.edit)" list="command-db">
</div> </div>
</pre> </pre>
</div> </div>
`; `;

View file

@ -27,409 +27,409 @@ const DFU_UPDATE_MODE_APP = 4;
// --- Utility Functions (adapted from dfu/util.py) --- // --- Utility Functions (adapted from dfu/util.py) ---
function int32ToBytes(value) { function int32ToBytes(value) {
const buffer = new ArrayBuffer(4); const buffer = new ArrayBuffer(4);
const view = new DataView(buffer); const view = new DataView(buffer);
view.setUint32(0, value, true); // Little-endian view.setUint32(0, value, true); // Little-endian
return new Uint8Array(buffer); return new Uint8Array(buffer);
} }
function int16ToBytes(value) { function int16ToBytes(value) {
const buffer = new ArrayBuffer(2); const buffer = new ArrayBuffer(2);
const view = new DataView(buffer); const view = new DataView(buffer);
view.setUint16(0, value, true); // Little-endian view.setUint16(0, value, true); // Little-endian
return new Uint8Array(buffer); return new Uint8Array(buffer);
} }
function slipPartsToFourBytes(seq, dip, rp, pktType, pktLen) { function slipPartsToFourBytes(seq, dip, rp, pktType, pktLen) {
const ints = new Uint8Array(4); const ints = new Uint8Array(4);
ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7); ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7);
ints[1] = pktType | ((pktLen & 0x000F) << 4); ints[1] = pktType | ((pktLen & 0x000F) << 4);
ints[2] = (pktLen & 0x0FF0) >> 4; ints[2] = (pktLen & 0x0FF0) >> 4;
ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF; ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF;
return ints; return ints;
} }
function slipEncodeEscChars(data) { function slipEncodeEscChars(data) {
const result = []; const result = [];
for (const byte of data) { for (const byte of data) {
if (byte === 0xC0) { if (byte === 0xC0) {
result.push(0xDB, 0xDC); result.push(0xDB, 0xDC);
} else if (byte === 0xDB) { } else if (byte === 0xDB) {
result.push(0xDB, 0xDD); result.push(0xDB, 0xDD);
} else { } else {
result.push(byte); result.push(byte);
}
} }
return new Uint8Array(result); }
return new Uint8Array(result);
} }
// --- CRC16 Calculation (adapted from dfu/crc16.py) --- // --- CRC16 Calculation (adapted from dfu/crc16.py) ---
function calcCrc16(data, crc = 0xFFFF) { function calcCrc16(data, crc = 0xFFFF) {
if (!(data instanceof Uint8Array)) { if (!(data instanceof Uint8Array)) {
throw new Error("calcCrc16 requires Uint8Array input"); throw new Error("calcCrc16 requires Uint8Array input");
} }
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
crc = ((crc >> 8) & 0x00FF) | ((crc << 8) & 0xFF00); crc = ((crc >> 8) & 0x00FF) | ((crc << 8) & 0xFF00);
crc ^= data[i]; crc ^= data[i];
crc ^= (crc & 0x00FF) >> 4; crc ^= (crc & 0x00FF) >> 4;
crc ^= (crc << 8) << 4; crc ^= (crc << 8) << 4;
crc ^= ((crc & 0x00FF) << 4) << 1; crc ^= ((crc & 0x00FF) << 4) << 1;
} }
return crc & 0xFFFF; return crc & 0xFFFF;
} }
function sleep(milliseconds) { function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds)) return new Promise((resolve) => setTimeout(resolve, milliseconds))
} }
// --- HciPacket Class (adapted from dfu/dfu_transport_serial.py) --- // --- HciPacket Class (adapted from dfu/dfu_transport_serial.py) ---
class HciPacket { class HciPacket {
static sequenceNumber = 0; static sequenceNumber = 0;
constructor(data) { constructor(data) {
HciPacket.sequenceNumber = (HciPacket.sequenceNumber + 1) % 8; HciPacket.sequenceNumber = (HciPacket.sequenceNumber + 1) % 8;
let tempData = []; let tempData = [];
const slipBytes = slipPartsToFourBytes( const slipBytes = slipPartsToFourBytes(
HciPacket.sequenceNumber, HciPacket.sequenceNumber,
DATA_INTEGRITY_CHECK_PRESENT, DATA_INTEGRITY_CHECK_PRESENT,
RELIABLE_PACKET, RELIABLE_PACKET,
HCI_PACKET_TYPE, HCI_PACKET_TYPE,
data.length data.length
); );
tempData = tempData.concat(Array.from(slipBytes)); tempData = tempData.concat(Array.from(slipBytes));
tempData = tempData.concat(Array.from(data)); tempData = tempData.concat(Array.from(data));
// Add CRC // Add CRC
const crc = calcCrc16(new Uint8Array(tempData)); const crc = calcCrc16(new Uint8Array(tempData));
tempData.push(crc & 0xFF); tempData.push(crc & 0xFF);
tempData.push((crc & 0xFF00) >> 8); tempData.push((crc & 0xFF00) >> 8);
const encoded = slipEncodeEscChars(new Uint8Array(tempData)); const encoded = slipEncodeEscChars(new Uint8Array(tempData));
this.data = new Uint8Array([0xC0, ...encoded, 0xC0]); this.data = new Uint8Array([0xC0, ...encoded, 0xC0]);
} }
} }
// --- Main DFU Class --- // --- Main DFU Class ---
export class Dfu { export class Dfu {
/** /**
* @param {SerialPort} port - The Web Serial API port object. * @param {SerialPort} port - The Web Serial API port object.
* @param {boolean} [eraseBeforeUpdate=false] - Whether to erase the entire flash before updating. * @param {boolean} [eraseBeforeUpdate=false] - Whether to erase the entire flash before updating.
*/ */
constructor(port, eraseBeforeUpdate = false) { constructor(port, eraseBeforeUpdate = false) {
this.port = port; this.port = port;
this.transferInProgress = false; this.transferInProgress = false;
this.lastAck = -1; this.lastAck = -1;
this.eraseBeforeUpdate = eraseBeforeUpdate; // Store the erase flag 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.");
} }
async sendPacket(pkt) { const writer = this.port.writable.getWriter();
if (!this.port || !this.port.writable) { try {
throw new Error("Serial port not open or not writable."); await writer.write(pkt.data);
} console.debug("Sent packet:", pkt.data);
} finally {
const writer = this.port.writable.getWriter(); writer.releaseLock();
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() { await this.getAck(); // Wait for ACK after sending
if (!this.port || !this.port.readable) { }
throw new Error("Serial port not open or not readable.");
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.");
} }
const reader = this.port.readable.getReader(); if (value) {
let buffer = []; for (const byte of value) {
let c0Count = 0; buffer.push(byte);
if (byte === 0xC0) {
try { c0Count++;
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; if (Date.now() - startTime > DEFAULT_SERIAL_PORT_TIMEOUT * 1000 * 5) { // Increased timeout for safety
// Send firmware packets HciPacket.sequenceNumber = 0; // Reset sequence number on timeout.
for (const [index, pkt] of frames.entries()) { throw new Error("Timeout waiting for ACK.");
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);
}
} }
}
} finally {
reader.releaseLock();
}
// Decode SLIP
const decodedData = this.decodeSlip(buffer);
// Wait for the last page to be written // 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); 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) { // Wait for the last page to be written
if (this.transferInProgress) { await sleep(FLASH_PAGE_WRITE_TIME * 1000);
throw new Error("DFU update already in progress.");
// 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());
} }
this.transferInProgress = true; }
this.lastAck = -1; // Reset last ACK
const decoder = new TextDecoder(); 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 { try {
await this.port.open({ baudRate: 115200 }); // Open with correct baudrate const reader = this.port.readable.getReader();
await reader.cancel();
const reader = new zip.ZipReader(new zip.BlobReader(zipFile)); reader.releaseLock();
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) { } catch (error) {
console.error("DFU Update failed:", error); // Ignore errors when trying to cancel the reader
throw error; // Re-throw the error for handling by the caller console.debug(`Error: closing reader: ${error}`);
} 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}`);
}
}
} }
}
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}`);
}
}
} }
}
} }