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 configRes = await fetch('./config.json'); const config = await configRes.json(); const githubRes = await fetch('/releases'); const github = await githubRes.json(); const commandReference = { '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 freq ': 'Set frequency {Mhz}', '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', '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', }; async function delay(milis) { return await new Promise((resolve) => setTimeout(resolve, milis)); } function getGithubReleases(roleType, files) { const versions = {}; for(const [fileType, matchRE] of Object.entries(files)) { 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) { if(!new RegExp(matchRE).test(file.name)) { continue } 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); // clean versions without files for(const [verName, verValue] of Object.entries(firmware.version)) { if(verValue.files.length === 0) delete firmware.version[verName] } } } config.device = config.device.filter(device => device.firmware.some(firmware => Object.keys(firmware.version).length > 0 )); return config; } 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; } console.log(addGithubFiles()); function setup() { const consoleEditBox = ref(); const consoleWindow = ref(); const deviceFilterText = ref(''); const snackbar = reactive({ text: '', class: '', icon: '', }); const selected = reactive({ device: null, firmware: null, version: null, wipe: false, nrfEraserFlashingPercent: 0, nrfEraserFlashing: false, port: null, }); const getRoleFwValue = (firmware, key) => { const role = config.role[firmware.role] ?? {}; return firmware[key] ?? role[key] ?? ''; } const getSelFwValue = (key) => { const fwVersion = selected.firmware.version[selected.version]; return fwVersion ? fwVersion[key] || '' : ''; } const flashing = reactive({ supported: 'Serial' in window, instance: null, locked: false, percent: 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 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() } 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; } 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; } } watch(() => selected.firmware, (firmware) => { if(firmware == null) return; selected.version = Object.keys(firmware.version)[0]; }); const flasherCleanup = async () => { flashing.active = false; flashing.log = ''; flashing.error = ''; flashing.dfuComplete = false; flashing.percent = 0; selected.firmware = null; selected.version = null; selected.wipe = false; selected.device = null; selected.nrfEraserFlashingPercent = 0; selected.nrfEraserFlashing = false; if(flashing.instance instanceof ESPLoader) { await flashing.instance?.hr.reset(); await flashing.instance?.transport?.disconnect(); } else if(flashing.instance instanceof Dfu) { try { flashing.instance.port.close() } catch(e) { console.error(e); } } flashing.instance = null; } 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'); } const openSerialCon = async() => { const port = selected.port = await navigator.serial.requestPort(); const serialConsole = serialCon.instance = new SerialConsole(port); 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'; 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 customFirmwareLoad = async(ev) => { const firmwareFile = ev.target.files[0]; const type = firmwareFile.name.endsWith('.bin') ? 'esp32' : 'nrf52'; selected.device = { name: 'Custom device', type, }; 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; } 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 }] } } 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' } const flashDevice = async() => { const device = selected.device; const firmware = selected.firmware.version[selected.version]; const flashFiles = firmware.files.filter(f => f.type.startsWith('flash')); if(!flashFiles[0]) { alert('Cannot find configuration for flash file! please report this to Discord.') flasherCleanup(); return; } console.log({flashFiles}); let flashData; if(flashFiles[0].file) { flashData = flashFiles[0].file; } else { let flashFile; if(device.type === 'esp32') { flashFile = flashFiles.find(f => f.type === (selected.wipe ? 'flash-wipe' : 'flash-update')); } else { flashFile = flashFiles[0]; } console.log({flashFiles, flashFile}); const url = getFirmwarePath(flashFile); 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; } flashData = await resp.blob(); } const port = selected.port = await navigator.serial.requestPort({}); if(device.type === 'esp32') { let esploader; let transport; const flashOptions = { terminal: log, compress: true, eraseAll: selected.wipe, flashSize: 'keep', flashMode: 'keep', flashFreq: 'keep', baudrate: 115200, romBaudrate: 115200, enableTracing: false, fileArray: [{ data: await blobToBinaryString(flashData), address: selected.wipe ? 0x00000 : 0x10000 }], reportProgress: async (_, written, total) => { flashing.percent = (written / total) * 100; }, }; 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 delay(100); await esploader.after('hard_reset'); await delay(100); await espReset(transport); await transport.disconnect(); } catch(e) { console.error(e); flashing.error = `ESP32 flashing failed: ${e}`; await espReset(transport); await transport.disconnect(); return; } } else if(device.type === 'nrf52') { const dfu = flashing.instance = new Dfu(port); flashing.active = true; try { await dfu.dfuUpdate(flashData, async (progress) => { flashing.percent = progress; }, 60000); } catch(e) { console.error(e); flashing.error = `nRF flashing failed: ${e}. Please reset the device and try again.`; return; } } }; const devices = computed(() => { const classSortPrefix = (d) => d.class === 'ripple' ? '1' : '2'; const deviceGroups = {}; for(const cls of ['ripple', 'community']) { const devices = config.device.toSorted( (a, b) => (classSortPrefix(a) + a.maker + a.name).localeCompare(classSortPrefix(b) + b.maker + b.name) ).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(); } return { snackbar, consoleEditBox, consoleWindow, consoleMouseUp, config, devices, selected, flashing, deviceFilterText, flashDevice, flasherCleanup, dfuMode, serialCon, closeSerialCon, openSerialCon, sendCommand, openSerialGUI, retry, close, commandReference, stepBack, customFirmwareLoad, getFirmwarePath, getSelFwValue, getRoleFwValue, firmwareHasData, canFlash, nrfErase } } createApp({ setup }).mount('#app');