2026-03-26 13:14:06 +01:00
|
|
|
import "/lib/beer.min.js";
|
|
|
|
|
import { createApp, reactive, ref, nextTick, watch, computed } 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 searchParams = new URLSearchParams(location.search);
|
|
|
|
|
const configName = searchParams.get('config')?.replaceAll(/[^a-z_-]/g, '') ?? 'config';
|
|
|
|
|
const configRes = await fetch(`/${configName}.json`);
|
2025-03-09 16:29:43 +01:00
|
|
|
const config = await configRes.json();
|
|
|
|
|
|
|
|
|
|
const githubRes = await fetch('/releases');
|
|
|
|
|
const github = await githubRes.json();
|
|
|
|
|
|
2025-02-23 10:19:53 +01:00
|
|
|
const commandReference = {
|
2025-02-23 10:27:17 +01:00
|
|
|
'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',
|
2025-03-09 16:29:43 +01:00
|
|
|
'set freq ': 'Set frequency {Mhz}',
|
2025-02-23 10:27:17 +01:00
|
|
|
'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',
|
2025-03-09 16:29:43 +01:00
|
|
|
'get freq ': 'Get frequency (Mhz)',
|
|
|
|
|
'get af': 'Get Air-time factor',
|
|
|
|
|
'get tx': 'Get Tx power (dBm)',
|
|
|
|
|
'get repeat': 'Get repeater mode',
|
|
|
|
|
'get advert.interval': 'Get advert rebroadcast interval (minutes)',
|
|
|
|
|
'get name': 'Get advertisement name',
|
|
|
|
|
'get lat': 'Get the advertisement map latitude',
|
|
|
|
|
'get lon': 'Get the advertisement map longitude',
|
2025-02-23 10:19:53 +01:00
|
|
|
};
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
async function delay(milis) {
|
|
|
|
|
return await new Promise((resolve) => setTimeout(resolve, milis));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 13:14:06 +01:00
|
|
|
function toSlug(text) {
|
|
|
|
|
return String(text).toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9.]+/g, '-')
|
|
|
|
|
.replace(/^-|-$/g, '');
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
function getGithubReleases(roleType, files) {
|
|
|
|
|
const versions = {};
|
2025-05-04 10:11:29 +02:00
|
|
|
for(const [fileType, matchRE] of Object.entries(files)) {
|
2025-03-09 16:29:43 +01:00
|
|
|
for(const versionType of github) {
|
|
|
|
|
if(versionType.type !== roleType) { continue }
|
|
|
|
|
const version = versions[versionType.version] ??= {
|
|
|
|
|
notes: versionType.notes,
|
|
|
|
|
files: []
|
|
|
|
|
};
|
|
|
|
|
for(const file of versionType.files) {
|
2025-05-04 10:11:29 +02:00
|
|
|
if(!new RegExp(matchRE).test(file.name)) { continue }
|
2025-03-09 16:29:43 +01:00
|
|
|
version.files.push({
|
|
|
|
|
type: fileType,
|
|
|
|
|
name: file.url,
|
|
|
|
|
title: file.name,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return versions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addGithubFiles() {
|
|
|
|
|
for(const device of config.device) {
|
|
|
|
|
for(const firmware of device.firmware) {
|
|
|
|
|
const gDef = firmware.github;
|
|
|
|
|
if(!gDef?.files) { continue }
|
|
|
|
|
firmware.version = getGithubReleases(gDef.type, gDef.files);
|
2025-05-04 10:11:29 +02:00
|
|
|
|
|
|
|
|
// clean versions without files
|
|
|
|
|
for(const [verName, verValue] of Object.entries(firmware.version)) {
|
|
|
|
|
if(verValue.files.length === 0) delete firmware.version[verName]
|
|
|
|
|
}
|
2025-03-09 16:29:43 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
config.device = config.device.filter(device => device.firmware.some(firmware => Object.keys(firmware.version).length > 0 ));
|
|
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
async function digestMessage(message) {
|
|
|
|
|
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
|
|
|
|
const hashBuffer = await window.crypto.subtle.digest("SHA-256", msgUint8); // hash the message
|
|
|
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
|
|
|
|
|
|
|
|
|
const hashHex = hashArray
|
|
|
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
|
|
|
.join(""); // convert bytes to hex string
|
|
|
|
|
|
|
|
|
|
return hashHex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function blobToBinaryString(blob) {
|
|
|
|
|
const bytes = new Uint8Array(await blob.arrayBuffer())
|
|
|
|
|
let binString = '';
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
|
|
|
binString += String.fromCharCode(bytes[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return binString;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
console.log(addGithubFiles());
|
|
|
|
|
|
2025-02-23 10:19:53 +01:00
|
|
|
function setup() {
|
2025-02-23 10:27:17 +01:00
|
|
|
const consoleEditBox = ref();
|
|
|
|
|
const consoleWindow = ref();
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
const deviceFilterText = ref('');
|
|
|
|
|
|
|
|
|
|
const snackbar = reactive({
|
|
|
|
|
text: '',
|
|
|
|
|
class: '',
|
|
|
|
|
icon: '',
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const selected = reactive({
|
|
|
|
|
device: null,
|
|
|
|
|
firmware: null,
|
2025-03-09 16:29:43 +01:00
|
|
|
version: null,
|
2025-02-23 10:27:17 +01:00
|
|
|
wipe: false,
|
2026-03-26 13:14:06 +01:00
|
|
|
espFlashAddress: 0x10000,
|
2026-01-10 23:15:50 +01:00
|
|
|
nrfEraserFlashingPercent: 0,
|
|
|
|
|
nrfEraserFlashing: false,
|
2025-03-09 16:29:43 +01:00
|
|
|
port: null,
|
2025-02-23 10:27:17 +01:00
|
|
|
});
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
const getRoleFwValue = (firmware, key) => {
|
2026-01-10 23:15:50 +01:00
|
|
|
const role = config.role[firmware.role] ?? {};
|
|
|
|
|
|
|
|
|
|
return firmware[key] ?? role[key] ?? '';
|
2025-03-09 16:29:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getSelFwValue = (key) => {
|
|
|
|
|
const fwVersion = selected.firmware.version[selected.version];
|
|
|
|
|
|
|
|
|
|
return fwVersion ? fwVersion[key] || '' : '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 13:14:06 +01:00
|
|
|
const getNotice = (selected) => {
|
|
|
|
|
let notice = config.notice[selected.firmware.notice] || selected.firmware.notice || '';
|
|
|
|
|
|
|
|
|
|
if(notice) {
|
|
|
|
|
notice = notice.replaceAll(/\$\{(\w+)\}/g, (_, varName) => selected.device[varName] || '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return notice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatChangeLog = (changelog) => {
|
|
|
|
|
return changelog
|
|
|
|
|
.replace(/change log:\r?\n/i, '')
|
|
|
|
|
.replace(/^[-*] /mg, '')
|
|
|
|
|
.replace(/#(\d+)$/gm, `<a target="_blank" href="https://github.com/meshcore-dev/MeshCore/pull/$1">#$1</a>`)
|
|
|
|
|
// .split(/\r?\n/)
|
|
|
|
|
// .map(l => `* ${l}`)
|
|
|
|
|
// .join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const flashing = reactive({
|
2025-11-10 23:39:53 +01:00
|
|
|
supported: 'Serial' in window || 'serial' in window.navigator,
|
2025-02-23 10:27:17 +01:00
|
|
|
instance: null,
|
2026-01-10 23:15:50 +01:00
|
|
|
locked: false,
|
|
|
|
|
percent: 0,
|
2025-02-23 10:27:17 +01:00
|
|
|
log: '',
|
|
|
|
|
error: '',
|
|
|
|
|
dfuComplete: false,
|
|
|
|
|
});
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const serialCon = reactive({
|
|
|
|
|
instance: null,
|
|
|
|
|
opened: false,
|
|
|
|
|
content: '',
|
|
|
|
|
edit: '',
|
|
|
|
|
});
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
window.app = { selected, flashing, serialCon };
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const log = {
|
|
|
|
|
clean() { flashing.log = '' },
|
|
|
|
|
write(data) { flashing.log += data },
|
|
|
|
|
writeLine(data) { flashing.log += data + '\n' }
|
|
|
|
|
};
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
const retry = async() => {
|
|
|
|
|
flashing.active = false;
|
|
|
|
|
flashing.log = '';
|
|
|
|
|
flashing.error = '';
|
|
|
|
|
flashing.dfuComplete = false;
|
|
|
|
|
flashing.percent = 0;
|
|
|
|
|
if(flashing.instance instanceof ESPLoader) {
|
|
|
|
|
await flashing.instance?.hr.reset();
|
|
|
|
|
await flashing.instance?.transport?.disconnect();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
|
|
|
|
location.reload()
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
const getFirmwarePath = (file) => {
|
|
|
|
|
return file.name.startsWith('/') ? file.name : `${config.staticPath}/${file.name}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const firmwareHasData = (firmware) => {
|
|
|
|
|
const firstVersion = Object.keys(firmware.version)[0];
|
|
|
|
|
if(!firstVersion) return false;
|
|
|
|
|
|
|
|
|
|
return firmware.version[firstVersion].files.length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 13:14:06 +01:00
|
|
|
// --- URL Routing ---
|
|
|
|
|
// NOTE: the server must serve index.html for all paths (catch-all / try_files).
|
|
|
|
|
|
|
|
|
|
const deviceToSlug = (device) => toSlug([device.class, device.name].join('-'));
|
|
|
|
|
|
|
|
|
|
const firmwareToSlug = (firmware) => {
|
|
|
|
|
const title = getRoleFwValue(firmware, 'title');
|
|
|
|
|
const subTitle = getRoleFwValue(firmware, 'subTitle');
|
|
|
|
|
return toSlug(subTitle ? `${title}-${subTitle}` : title);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let initializingFromUrl = false;
|
|
|
|
|
|
|
|
|
|
const buildUrl = () => {
|
|
|
|
|
if (serialCon.opened) return '/console';
|
|
|
|
|
if (!selected.device) return '/';
|
|
|
|
|
let path = '/' + deviceToSlug(selected.device) + '/';
|
|
|
|
|
if (!selected.firmware) return path;
|
|
|
|
|
path += firmwareToSlug(selected.firmware) + '/';
|
|
|
|
|
if (selected.version) path += toSlug(selected.version);
|
|
|
|
|
return path;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateUrl = (replace = false) => {
|
|
|
|
|
if (initializingFromUrl) return;
|
|
|
|
|
const path = buildUrl();
|
|
|
|
|
if (window.location.pathname !== path) {
|
|
|
|
|
replace ? history.replaceState(null, '', path) : history.pushState(null, '', path);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const applyUrlPath = (path) => {
|
|
|
|
|
initializingFromUrl = true;
|
|
|
|
|
const segments = path.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
|
|
|
|
|
|
|
|
|
|
if (segments.length === 0 || segments[0] === 'console') {
|
|
|
|
|
nextTick(() => { initializingFromUrl = false; });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [deviceSlug, roleSlug, versionSlug] = segments;
|
|
|
|
|
const matchingDevices = config.device.filter(d => deviceToSlug(d) === deviceSlug);
|
|
|
|
|
if (matchingDevices.length === 0) {
|
|
|
|
|
nextTick(() => { initializingFromUrl = false; });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When multiple devices share the same slug, use the firmware slug to pick the right one
|
|
|
|
|
let device, firmware;
|
|
|
|
|
if (roleSlug && matchingDevices.length > 1) {
|
|
|
|
|
for (const d of matchingDevices) {
|
|
|
|
|
const f = d.firmware.find(f => firmwareToSlug(f) === roleSlug && firmwareHasData(f));
|
|
|
|
|
if (f) { device = d; firmware = f; break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!device) device = matchingDevices[0];
|
|
|
|
|
selected.device = device;
|
|
|
|
|
|
|
|
|
|
if (!roleSlug) {
|
|
|
|
|
nextTick(() => { initializingFromUrl = false; });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!firmware) firmware = device.firmware.find(f => firmwareToSlug(f) === roleSlug && firmwareHasData(f));
|
|
|
|
|
if (!firmware) {
|
|
|
|
|
nextTick(() => { initializingFromUrl = false; });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
selected.firmware = firmware;
|
|
|
|
|
|
|
|
|
|
// Use nextTick so the firmware watcher sets the default version first,
|
|
|
|
|
// then we override it with the version from the URL.
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
if (versionSlug) {
|
|
|
|
|
const versionName = Object.keys(firmware.version).find(v => toSlug(v) === versionSlug);
|
|
|
|
|
if (versionName) selected.version = versionName;
|
|
|
|
|
}
|
|
|
|
|
initializingFromUrl = false;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
const stepBack = () => {
|
|
|
|
|
if(selected.device && selected.firmware) {
|
|
|
|
|
if(selected.firmware.version[selected.version].customFile) {
|
|
|
|
|
selected.firmware = null;
|
|
|
|
|
selected.device = null;
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selected.firmware = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(selected.device) {
|
|
|
|
|
selected.device = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const flasherCleanup = async () => {
|
|
|
|
|
flashing.active = false;
|
|
|
|
|
flashing.log = '';
|
|
|
|
|
flashing.error = '';
|
|
|
|
|
flashing.dfuComplete = false;
|
2026-01-10 23:15:50 +01:00
|
|
|
flashing.percent = 0;
|
2025-02-23 10:27:17 +01:00
|
|
|
selected.firmware = null;
|
2025-03-09 16:29:43 +01:00
|
|
|
selected.version = null;
|
2025-02-23 10:27:17 +01:00
|
|
|
selected.wipe = false;
|
|
|
|
|
selected.device = null;
|
2026-01-10 23:15:50 +01:00
|
|
|
selected.nrfEraserFlashingPercent = 0;
|
|
|
|
|
selected.nrfEraserFlashing = false;
|
2025-02-23 10:27:17 +01:00
|
|
|
if(flashing.instance instanceof ESPLoader) {
|
|
|
|
|
await flashing.instance?.hr.reset();
|
|
|
|
|
await flashing.instance?.transport?.disconnect();
|
|
|
|
|
}
|
2026-01-10 23:15:50 +01:00
|
|
|
else if(flashing.instance instanceof Dfu) {
|
|
|
|
|
try {
|
|
|
|
|
flashing.instance.port.close()
|
|
|
|
|
}
|
|
|
|
|
catch(e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
flashing.instance = null;
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-05-04 10:11:29 +02:00
|
|
|
const openSerialGUI = () => {
|
|
|
|
|
window.open('https://config.meshcore.dev','meshcore_config','directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=1000,height=800');
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const openSerialCon = async() => {
|
|
|
|
|
const port = selected.port = await navigator.serial.requestPort();
|
|
|
|
|
const serialConsole = serialCon.instance = new SerialConsole(port);
|
2026-01-10 23:15:50 +01:00
|
|
|
|
|
|
|
|
serialCon.content = '-------------------------------------------------------------------------\n';
|
|
|
|
|
serialCon.content += 'Welcome to MeshCore serial console.\n'
|
|
|
|
|
serialCon.content += 'Click on the cursor to get all supported commands.\n';
|
|
|
|
|
serialCon.content += '-------------------------------------------------------------------------\n\n';
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
serialConsole.onOutput = (text) => {
|
|
|
|
|
serialCon.content += text;
|
|
|
|
|
};
|
|
|
|
|
serialConsole.connect();
|
|
|
|
|
serialCon.opened = true;
|
|
|
|
|
await nextTick();
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
consoleEditBox.value.focus();
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const closeSerialCon = async() => {
|
|
|
|
|
serialCon.opened = false;
|
|
|
|
|
await serialCon.instance.disconnect();
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const sendCommand = async(text) => {
|
|
|
|
|
const consoleEl = consoleWindow.value;
|
|
|
|
|
serialCon.edit = '';
|
|
|
|
|
await serialCon.instance.sendCommand(text);
|
|
|
|
|
setTimeout(() => consoleEl.scrollTop = consoleEl.scrollHeight, 100);
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const dfuMode = async() => {
|
|
|
|
|
await Dfu.forceDfuMode(await navigator.serial.requestPort({}))
|
|
|
|
|
flashing.dfuComplete = true;
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
const customFirmwareLoad = async(ev) => {
|
|
|
|
|
const firmwareFile = ev.target.files[0];
|
|
|
|
|
const type = firmwareFile.name.endsWith('.bin') ? 'esp32' : 'nrf52';
|
2026-01-10 23:15:50 +01:00
|
|
|
selected.device = {
|
2025-03-09 16:29:43 +01:00
|
|
|
name: 'Custom device',
|
|
|
|
|
type,
|
|
|
|
|
};
|
2026-01-10 23:15:50 +01:00
|
|
|
if(firmwareFile.name.endsWith('-merged.bin')) {
|
|
|
|
|
alert(
|
|
|
|
|
'You selected custom file that ends with "merged.bin".'+
|
|
|
|
|
'This will erase your flash! Proceed with caution.'+
|
|
|
|
|
'If you want just to update your firmware, please use non-merged bin.'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
selected.wipe = true;
|
2026-03-26 13:14:06 +01:00
|
|
|
selected.espFlashAddress = 0;
|
2026-01-10 23:15:50 +01:00
|
|
|
}
|
2025-03-09 16:29:43 +01:00
|
|
|
|
|
|
|
|
selected.firmware = {
|
|
|
|
|
icon: 'unknown_document',
|
|
|
|
|
title: firmwareFile.name,
|
|
|
|
|
version: {},
|
|
|
|
|
}
|
|
|
|
|
selected.version = firmwareFile.name;
|
|
|
|
|
selected.firmware.version[selected.version] = {
|
|
|
|
|
customFile: true,
|
|
|
|
|
files: [{ type: 'flash', file: firmwareFile }]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
const espReset = async(t) => {
|
|
|
|
|
await t.setRTS(true);
|
|
|
|
|
await delay(100)
|
|
|
|
|
await t.setRTS(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nrfErase = async() => {
|
|
|
|
|
if(!(selected.device.type === 'nrf52' && selected.device.erase)) {
|
|
|
|
|
console.error('nRF erase called for non-nrf device or device.erase is not defined')
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const url = `${config.staticPath}/${selected.device.erase}`;
|
|
|
|
|
|
|
|
|
|
console.log('downloading: ' + url);
|
|
|
|
|
const resp = await fetch(url);
|
|
|
|
|
if(resp.status !== 200) {
|
|
|
|
|
alert(`Could not download the firmware file from the server, reported: HTTP ${resp.status}.\nPlease try again.`)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const flashData = await resp.blob();
|
|
|
|
|
|
|
|
|
|
const port = selected.port = await navigator.serial.requestPort({});
|
|
|
|
|
const dfu = new Dfu(port);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
selected.nrfEraserFlashing = true;
|
|
|
|
|
await dfu.dfuUpdate(flashData, async (progress) => {
|
|
|
|
|
selected.nrfEraserFlashingPercent = progress;
|
|
|
|
|
if(progress === 100 && selected.nrfEraserFlashing) {
|
|
|
|
|
selected.nrfEraserFlashing = false;
|
|
|
|
|
selected.dfuComplete = false;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
alert('Device erase firmware has been flashed and flash has been erased.\nYou can flash MeshCore now.');
|
|
|
|
|
}, 200);
|
|
|
|
|
}
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
catch(e) {
|
|
|
|
|
alert(`nRF flashing erase firmware failed: ${e}.\nDid you put the device into DFU mode before attempting erasing?`);
|
|
|
|
|
selected.nrfEraserFlashing = false;
|
|
|
|
|
selected.nrfEraserFlashingPercent = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const canFlash = (device) => {
|
|
|
|
|
return device.type !== 'noflash'
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const flashDevice = async() => {
|
|
|
|
|
const device = selected.device;
|
2025-03-09 16:29:43 +01:00
|
|
|
const firmware = selected.firmware.version[selected.version];
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
const flashFiles = firmware.files.filter(f => f.type.startsWith('flash'));
|
|
|
|
|
if(!flashFiles[0]) {
|
2025-02-23 10:27:17 +01:00
|
|
|
alert('Cannot find configuration for flash file! please report this to Discord.')
|
|
|
|
|
flasherCleanup();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-03-09 16:29:43 +01:00
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
let flashData;
|
|
|
|
|
if(flashFiles[0].file) {
|
|
|
|
|
flashData = flashFiles[0].file;
|
2025-03-09 16:29:43 +01:00
|
|
|
} else {
|
2026-01-10 23:15:50 +01:00
|
|
|
let flashFile;
|
|
|
|
|
if(device.type === 'esp32') {
|
|
|
|
|
flashFile = flashFiles.find(f => f.type === (selected.wipe ? 'flash-wipe' : 'flash-update'));
|
2026-03-26 13:14:06 +01:00
|
|
|
if(selected.wipe) selected.espFlashAddress = 0x00000;
|
2026-01-10 23:15:50 +01:00
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
flashFile = flashFiles[0];
|
|
|
|
|
}
|
|
|
|
|
console.log({flashFiles, flashFile});
|
|
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
const url = getFirmwarePath(flashFile);
|
|
|
|
|
console.log('downloading: ' + url);
|
|
|
|
|
const resp = await fetch(url);
|
2026-01-10 23:15:50 +01:00
|
|
|
if(resp.status !== 200) {
|
|
|
|
|
alert(`Could not download the firmware file from the server, reported: HTTP ${resp.status}.\nPlease try again.`)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flashData = await resp.blob();
|
2025-03-09 16:29:43 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const port = selected.port = await navigator.serial.requestPort({});
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
if(device.type === 'esp32') {
|
|
|
|
|
let esploader;
|
|
|
|
|
let transport;
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const flashOptions = {
|
|
|
|
|
terminal: log,
|
|
|
|
|
compress: true,
|
|
|
|
|
eraseAll: selected.wipe,
|
|
|
|
|
flashSize: 'keep',
|
|
|
|
|
flashMode: 'keep',
|
|
|
|
|
flashFreq: 'keep',
|
|
|
|
|
baudrate: 115200,
|
|
|
|
|
romBaudrate: 115200,
|
|
|
|
|
enableTracing: false,
|
|
|
|
|
fileArray: [{
|
2026-01-10 23:15:50 +01:00
|
|
|
data: await blobToBinaryString(flashData),
|
2026-03-26 13:14:06 +01:00
|
|
|
address: selected.espFlashAddress
|
2025-02-23 10:27:17 +01:00
|
|
|
}],
|
2025-03-09 16:29:43 +01:00
|
|
|
reportProgress: async (_, written, total) => {
|
2026-01-10 23:15:50 +01:00
|
|
|
flashing.percent = (written / total) * 100;
|
2025-02-23 10:27:17 +01:00
|
|
|
},
|
|
|
|
|
};
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
try {
|
|
|
|
|
flashing.active = true;
|
|
|
|
|
transport = new Transport(port, true);
|
2026-01-10 23:15:50 +01:00
|
|
|
flashOptions.transport = transport;
|
2025-02-23 10:27:17 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
try {
|
|
|
|
|
await esploader.writeFlash(flashOptions);
|
2026-01-10 23:15:50 +01:00
|
|
|
await delay(100);
|
|
|
|
|
await esploader.after('hard_reset');
|
|
|
|
|
await delay(100);
|
|
|
|
|
await espReset(transport);
|
|
|
|
|
await transport.disconnect();
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
|
|
|
|
catch(e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
flashing.error = `ESP32 flashing failed: ${e}`;
|
2026-01-10 23:15:50 +01:00
|
|
|
await espReset(transport);
|
2025-02-23 10:27:17 +01:00
|
|
|
await transport.disconnect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if(device.type === 'nrf52') {
|
2026-01-10 23:15:50 +01:00
|
|
|
const dfu = flashing.instance = new Dfu(port);
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
flashing.active = true;
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
try {
|
2026-01-10 23:15:50 +01:00
|
|
|
await dfu.dfuUpdate(flashData, async (progress) => {
|
|
|
|
|
flashing.percent = progress;
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
|
|
|
|
catch(e) {
|
|
|
|
|
console.error(e);
|
2026-01-10 23:15:50 +01:00
|
|
|
flashing.error = `nRF flashing failed: ${e}. Please reset the device and try again.`;
|
2025-02-23 10:27:17 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
const devices = computed(() => {
|
2026-03-26 13:14:06 +01:00
|
|
|
const classes = ['ripple', 'meshos', 'community'];
|
2026-01-10 23:15:50 +01:00
|
|
|
const deviceGroups = {};
|
|
|
|
|
|
2026-03-26 13:14:06 +01:00
|
|
|
let index = 0;
|
|
|
|
|
for(const cls of classes) {
|
2026-01-10 23:15:50 +01:00
|
|
|
const devices = config.device.toSorted(
|
2026-03-26 13:14:06 +01:00
|
|
|
(a, b) => (index + a.maker + a.name).localeCompare(index + b.maker + b.name)
|
2026-01-10 23:15:50 +01:00
|
|
|
).filter(
|
|
|
|
|
d => d.class === cls && (deviceFilterText.value == '' || d.name.toLowerCase().includes(deviceFilterText.value?.toLowerCase()))
|
|
|
|
|
)
|
|
|
|
|
if(devices.length > 0) deviceGroups[cls] = devices;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return deviceGroups;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const showMessage = (text, icon, displayMs) => {
|
|
|
|
|
snackbar.class = 'active';
|
|
|
|
|
snackbar.text = text;
|
|
|
|
|
snackbar.icon = icon || '';
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
snackbar.icon = '';
|
|
|
|
|
snackbar.text = '';
|
|
|
|
|
snackbar.class = '';
|
|
|
|
|
}, displayMs || 2000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const consoleMouseUp = (ev) => {
|
|
|
|
|
if(window.getSelection().toString().length) {
|
|
|
|
|
navigator.clipboard.writeText(window.getSelection().toString())
|
|
|
|
|
showMessage('text copied to clipboard');
|
|
|
|
|
}
|
|
|
|
|
consoleEditBox.value.focus();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 13:14:06 +01:00
|
|
|
watch(() => selected.firmware, (firmware) => {
|
|
|
|
|
if(firmware == null) return;
|
|
|
|
|
selected.version = Object.keys(firmware.version)[0];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(() => selected.device, updateUrl);
|
|
|
|
|
watch(() => selected.firmware, updateUrl);
|
|
|
|
|
watch(() => selected.version, () => updateUrl(true)); // replace: version is a refinement, not a new nav step
|
|
|
|
|
watch(() => serialCon.opened, updateUrl);
|
|
|
|
|
|
|
|
|
|
window.addEventListener('popstate', () => {
|
|
|
|
|
if (serialCon.opened) closeSerialCon();
|
|
|
|
|
flashing.active = false;
|
|
|
|
|
flashing.log = '';
|
|
|
|
|
flashing.error = '';
|
|
|
|
|
selected.firmware = null;
|
|
|
|
|
selected.version = null;
|
|
|
|
|
selected.device = null;
|
|
|
|
|
applyUrlPath(window.location.pathname);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
applyUrlPath(window.location.pathname);
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
return {
|
2026-01-10 23:15:50 +01:00
|
|
|
snackbar,
|
|
|
|
|
consoleEditBox, consoleWindow, consoleMouseUp,
|
|
|
|
|
config, devices, selected, flashing, deviceFilterText,
|
2025-02-23 10:27:17 +01:00
|
|
|
flashDevice, flasherCleanup, dfuMode,
|
2025-05-04 10:11:29 +02:00
|
|
|
serialCon, closeSerialCon, openSerialCon,
|
|
|
|
|
sendCommand, openSerialGUI,
|
2026-01-10 23:15:50 +01:00
|
|
|
retry, close, commandReference,
|
2025-03-09 16:29:43 +01:00
|
|
|
stepBack,
|
2026-03-26 13:14:06 +01:00
|
|
|
customFirmwareLoad, getFirmwarePath,
|
|
|
|
|
getSelFwValue, getRoleFwValue, getNotice, formatChangeLog,
|
2026-01-10 23:15:50 +01:00
|
|
|
firmwareHasData,
|
|
|
|
|
canFlash, nrfErase
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-09 16:29:43 +01:00
|
|
|
createApp({ setup }).mount('#app');
|