import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.4.5/bundle.js"; /** * write all messages to the console */ class ConsoleOutputHandler{ clean() { } writeLine(data) { console.log("ESPInstaller:",data); } write(data) { console.log(data); } } /** * write messages to an instance of xterm * to use this, include in your html * and create a div element
* provide the id of this div to the constructor */ class XtermOutputHandler { constructor(termId) { let termElement = document.getElementById(termId); if (termElement) { this.term = new Terminal({ cols: 120, rows: 40 , convertEol: true }); this.term.open(termElement); } this.clean=this.clean.bind(this); this.writeLine=this.writeLine.bind(this); this.write=this.write.bind(this); } clean() { if (!this.term) return; this.term.clear(); } writeLine(data) { if (!this.term) { console.log("TERM:", data); return; }; this.term.writeln(data); } write(data) { if (!this.term) { console.log("TERM:", data); return; }; this.term.write(data) } }; class ESPInstaller{ constructor(outputHandler){ this.espLoaderTerminal=outputHandler|| new ConsoleOutputHandler(); this.transport=undefined; this.esploader=undefined; this.chipFamily=undefined; this.base=import.meta.url.replace(/[^/]*$/,"install.php"); this.consoleDevice=undefined; this.consoleReader=undefined; this.imageChipId=undefined; } /** * get an URL query parameter * @param key * @returns */ static getParam(key){ let value=RegExp(""+key+"[^&]+").exec(window.location.search); // Return the unescaped value minus everything starting from the equals sign or an empty string return decodeURIComponent(!!value ? value.toString().replace(/^[^=]+./,"") : ""); }; static checkAvailable(){ if (! navigator.serial || ! navigator.serial.requestPort) return false; return true; } /** * execute a reset on the connected device */ async resetTransport() { if (!this.transport) { throw new Error("not connected"); } this.espLoaderTerminal.writeLine("Resetting..."); await this.transport.device.setSignals({ dataTerminalReady: false, requestToSend: true, }); await this.transport.device.setSignals({ dataTerminalReady: false, requestToSend: false, }); }; async disconnect(){ if (this.consoleDevice){ try{ if (this.consoleReader){ await this.consoleReader.cancel(); this.consoleReader=undefined; } }catch(e){ console.log(`error cancel serial read ${e}`); } try{ await this.consoleDevice.close(); }catch(e){ console.log('error closing console device', this.consoleDevice,e); } this.consoleDevice=undefined; } if (this.transport){ try{ await this.transport.disconnect(); await this.transport.waitForUnlock(1500); }catch (e){} this.transport=undefined; } this.esploader=undefined; } async connect() { this.espLoaderTerminal.clean(); await this.disconnect(); let device = await navigator.serial.requestPort({}); if (!device) { return; } try { this.transport = new Transport(device); this.esploader = new ESPLoader({ transport:this.transport, baudrate: 115200, terminal: this.espLoaderTerminal}); //this.esploader.debugLogging=true; let foundChip = await this.esploader.main(); if (!foundChip) { throw new Error("unable to read chip id"); } this.espLoaderTerminal.writeLine(`chip: ${foundChip}`); //await this.esploader.flashId(); this.chipFamily = this.esploader.chip.CHIP_NAME; this.imageChipId = this.esploader.chip.IMAGE_CHIP_ID; this.espLoaderTerminal.writeLine(`chipFamily: ${this.chipFamily}`); } catch (e) { this.disconnect(); throw e; } } async startConsole(baud) { await this.disconnect(); try { let device = await navigator.serial.requestPort({}); if (!device) { return; } this.consoleDevice=device; let br=baud || 115200; await device.open({ baudRate: br }); this.consoleReader=device.readable.getReader(); this.espLoaderTerminal.clean(); this.espLoaderTerminal.writeLine(`Console at ${br}:`); while (this.consoleReader) { let {value:val,done:done} = await this.consoleReader.read(); if (typeof val !== 'undefined') { this.espLoaderTerminal.write(val); } if (done){ console.log("Console reader stopped"); break; } } } catch (e) { this.espLoaderTerminal.writeLine(`Error: ${e}`) } this.espLoaderTerminal.writeLine("Console reader stopped"); } async stopConsole(){ await this.disconnect(); } isConnected(){ return this.transport !== undefined; } checkConnected(){ if (! this.isConnected){ throw new Error("not connected"); } } getChipFamily(){ this.checkConnected(); return this.chipFamily; } getChipId(){ this.checkConnected(); return this.imageChipId; } /** * flass the device * @param {*} fileList : an array of entries {data:blob,address:number} */ async writeFlash(fileList){ this.checkConnected(); this.espLoaderTerminal.writeLine(`Flashing....`); await this.esploader.writeFlash({ fileArray: fileList, flashSize: "keep", flashMode: "keep", flashFreq: "keep", eraseAll: false, compress: true, /*reportProgress: (fileIndex, written, total)=>{ this.espLoaderTerminal.writeLine(`file ${fileIndex}: ${written}/${total}`); }*/ } ) await this.resetTransport(); this.espLoaderTerminal.writeLine(`Done.`); } /** * fetch a release asset from github * @param {*} user * @param {*} repo * @param {*} version * @param {*} name * @returns */ async getReleaseAsset(user,repo,version,name){ const url=this.base+"?dlName="+encodeURIComponent(name)+ "&dlVersion="+encodeURIComponent(version)+ "&user="+encodeURIComponent(user)+ "&repo="+encodeURIComponent(repo); this.espLoaderTerminal.writeLine(`downloading image from ${url}`); const resp=await fetch(url); if (! resp.ok){ throw new Error(`unable to download image from ${url}: ${resp.status}`); } const reader=new FileReader(); const blob= await resp.blob(); let data=await new Promise((resolve)=>{ reader.addEventListener("load",() => resolve(reader.result)); reader.readAsBinaryString(blob); }); this.espLoaderTerminal.writeLine(`successfully loaded ${data.length} bytes`); return data; } /** * handle the click of an install button * @param {*} isFull * @param {*} user * @param {*} repo * @param {*} version * @param {*} assetName * @param {*} checkChip will be called with the found chipId and the data and the isFull flag * must return an info with the flashStart being set * @returns */ async installClicked(isFull, user, repo, version, assetName,checkChip) { try { await this.connect(); let imageData = await this.getReleaseAsset(user, repo, version, assetName); if (!imageData || imageData.length == 0) { throw new Error(`no image data fetched`); } let info=await checkChip(this.getChipId(),imageData,isFull); let fileList = [ { data: imageData, address: info.flashStart } ]; let txt = isFull ? "baseImage (all data will be erased)" : "update"; if (!confirm(`ready to install ${version}\n${txt}`)) { this.espLoaderTerminal.writeLine("aborted by user..."); await this.disconnect(); return; } await this.writeFlash(fileList); await this.disconnect(); } catch (e) { this.espLoaderTerminal.writeLine(`Error: ${e}`); alert(`Error: ${e}`); } } /** * directly run the flash * @param {*} isFull * @param {*} imageData the data to be flashed * @param {*} version the info shown in the dialog * @param {*} checkChip will be called with the found chipId and the data * must return an info with flashStart * @returns */ async runFlash(isFull,imageData,version,checkChip){ try { await this.connect(); let info= await checkChip(this.getChipId(),imageData,isFull); //just check let fileList = [ { data: imageData, address: info.flashStart } ]; let txt = isFull ? "baseImage (all data will be erased)" : "update"; if (!confirm(`ready to install ${version}\n${txt}`)) { this.espLoaderTerminal.writeLine("aborted by user..."); await this.disconnect(); return; } await this.writeFlash(fileList); await this.disconnect(); } catch (e) { this.espLoaderTerminal.writeLine(`Error: ${e}`); alert(`Error: ${e}`); } } /** * fetch the release info from the github API * @param {*} user * @param {*} repo * @returns */ async getReleaseInfo(user,repo){ let url=this.base+"?api=1&user="+encodeURIComponent(user)+"&repo="+encodeURIComponent(repo) let resp=await fetch(url); if (! resp.ok){ throw new Error(`unable to query release info from ${url}: ${resp.status}`); } return await resp.json(); } /** * get the release info in a parsed form * @param {*} user * @param {*} repo * @returns an object: {version:nnn, assets:[name1,name2,...]} */ async getParsedReleaseInfo(user,repo){ let raw=await this.getReleaseInfo(user,repo); let rt={ version:raw.name, assets:[] }; if (! raw.assets) return rt; raw.assets.forEach((asset)=>{ rt.assets.push(asset.name); }) return rt; } }; export {ConsoleOutputHandler, XtermOutputHandler}; export default ESPInstaller;