web-firmware-installer-js/dist/cpinstaller.min.js
2023-03-15 23:53:26 +00:00

106 lines
No EOL
28 KiB
JavaScript

"use strict";import{html}from"https://cdn.jsdelivr.net/npm/lit-html/+esm";import{map}from"https://cdn.jsdelivr.net/npm/lit-html/directives/map/+esm";import*as toml from"https://cdn.jsdelivr.net/npm/iarna-toml-esm@3.0.5/+esm";import*as zip from"https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.6.65/+esm";import*as esptoolPackage from"https://cdn.jsdelivr.net/npm/esp-web-flasher@5.1.2/dist/web/index.js/+esm";import{REPL}from"https://cdn.jsdelivr.net/gh/adafruit/circuitpython-repl-js/repl.js";import{InstallButton,ESP_ROM_BAUD}from"./base_installer.min.js";const PREFERRED_BAUDRATE=921600,COPY_CHUNK_SIZE=65536,DEFAULT_RELEASE_LATEST=!1,BOARD_DEFS="https://adafruit-circuit-python.s3.amazonaws.com/esp32_boards.json",CSS_DIALOG_CLASS="cp-installer-dialog",FAMILY_TO_CHIP_MAP={esp32s2:esptoolPackage.CHIP_FAMILY_ESP32S2,esp32s3:esptoolPackage.CHIP_FAMILY_ESP32S3,esp32c3:esptoolPackage.CHIP_FAMILY_ESP32C3,esp32:esptoolPackage.CHIP_FAMILY_ESP32},attrMap={bootloader:"bootloaderUrl",uf2file:"uf2FileUrl",binfile:"binFileUrl"};class CPInstallButton extends InstallButton{constructor(){super(),this.releaseVersion="[version]",this.boardName="ESP32-based device",this.boardIds=null,this.selectedBoardId=null,this.bootloaderUrl=null,this.boardDefs=null,this.uf2FileUrl=null,this.binFileUrl=null,this.releaseVersion=0,this.chipFamily=null,this.dialogCssClass=CSS_DIALOG_CLASS,this.dialogs={...this.dialogs,...this.cpDialogs},this.bootDriveHandle=null,this.circuitpyDriveHandle=null,this._bootDriveName=null,this._serialPortName=null,this.replSerialDevice=null,this.repl=null,this.fileCache=[],this.reader=null,this.writer=null,this.tomlSettings=null,this.init()}static get observedAttributes(){return Object.keys(attrMap)}parseVersion(e){var t={},e=e.match(/(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?/);return e&&4<=e.length&&(t.major=e[1],t.minor=e[2],t.patch=e[3],e[4]&&e[5]?(t.suffix=e[4],t.suffixVersion=e[5]):(t.suffix="stable",t.suffixVersion=0)),t}sortReleases(e){const r=["major","minor","patch","suffix","suffixVersion"];return e.sort((e,t)=>{var i,s=this.parseVersion(e.version),a=this.parseVersion(t.version);for(i of r){if(s[i]<a[i])return-1;if(s[i]>a[i])return 1}return 0}),e}async connectedCallback(){var e=await fetch(BOARD_DEFS),e=(this.boardDefs=await e.json(),this.getAttribute("boardid"));e&&0!==e.trim().length?this.boardIds=e.split(","):this.boardIds=Object.keys(this.boardDefs),1===this.boardIds.length&&(this.selectedBoardId=this.boardIds[0]),this.getAttribute("version")&&(this.releaseVersion=this.getAttribute("version")),super.connectedCallback()}async loadBoard(e){let t=null;if(Object.keys(this.boardDefs).includes(e)){e=this.boardDefs[e],e=(this.chipFamily=e.chipfamily,e.name&&(this.boardName=e.name),e.bootloader&&(this.bootloaderUrl=this.updateBinaryUrl(e.bootloader)),this.sortReleases(e.releases));if(this.releaseVersion)for(var i of e)if(i.version==this.releaseVersion){t=i;break}t||(t=DEFAULT_RELEASE_LATEST?e[e.length-1]:e[0],this.releaseVersion=t.version),t.uf2file&&(this.uf2FileUrl=this.updateBinaryUrl(t.uf2file)),t.binfile&&(this.binFileUrl=this.updateBinaryUrl(t.binfile))}this.getAttribute("chipfamily")&&(this.chipFamily=this.getAttribute("chipfamily")),this.getAttribute("boardname")&&(this.boardName=this.getAttribute("boardname")),this.menuTitle="CircuitPython Installer for "+this.boardName}attributeChangedCallback(e,t,i){this[attrMap[e]]=i?this.updateBinaryUrl(i):null}updateBinaryUrl(e){return e=e&&e.replace("https://downloads.circuitpython.org/","https://adafruit-circuit-python.s3.amazonaws.com/")}flows={uf2FullProgram:{label:"Full CircuitPython [version] Install",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepBootloader,this.stepSelectBootDrive,this.stepCopyUf2,this.stepSelectCpyDrive,this.stepCredentials,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()&&!!this.bootloaderUrl&&!!this.uf2FileUrl},binFullProgram:{label:"Full CircuitPython [version] Install",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepFlashBin,this.stepSetupRepl,this.stepCredentials,this.stepSuccess],isEnabled:async()=>!this.hasNativeUsb()&&!!this.binFileUrl},uf2Only:{label:"Upgrade/Install CircuitPython [version] UF2 Only",steps:[this.stepWelcome,this.stepSelectBootDrive,this.stepCopyUf2,this.stepSelectCpyDrive,this.stepCredentials,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()&&!!this.uf2FileUrl},binOnly:{label:"Upgrade CircuitPython [version] Bin Only",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepFlashBin,this.stepSuccess],isEnabled:async()=>!!this.binFileUrl},bootloaderOnly:{label:"Install Bootloader Only",steps:[this.stepWelcome,this.stepSerialConnect,this.stepConfirm,this.stepEraseAll,this.stepBootloader,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()&&!!this.bootloaderUrl},credentialsOnlyRepl:{label:"Update WiFi credentials",steps:[this.stepWelcome,this.stepSetupRepl,this.stepCredentials,this.stepSuccess],isEnabled:async()=>!this.hasNativeUsb()},credentialsOnlyDrive:{label:"Update WiFi credentials",steps:[this.stepWelcome,this.stepSelectCpyDrive,this.stepCredentials,this.stepSuccess],isEnabled:async()=>this.hasNativeUsb()}};cpDialogs={boardSelect:{closeable:!0,template:i=>html`
<p>
There are multiple boards are available. Select the board you have:
</p>
<p>
<select id="availableBoards">
<option value="0"> - boards - </option>
${map(i.boards,(e,t)=>html`<option value="${e.id}" ${e.id==i.default?"selected":""}>${e.name}</option>`)}
</select>
</p>
`,buttons:[{label:"Select Board",onClick:this.selectBoardHandler,isEnabled:async()=>"0"!=this.currentDialogElement.querySelector("#availableBoards").value}]},welcome:{closeable:!0,template:e=>html`
<p>
Welcome to the CircuitPython Installer. This tool will install CircuitPython on your ${e.boardName}.
</p>
<p>
This tool is <strong>new</strong> and <strong>experimental</strong>. If you experience any issues, feel free to check out
<a href="https://github.com/adafruit/circuitpython-org/issues">https://github.com/adafruit/circuitpython-org/issues</a>
to see if somebody has already submitted the same issue you are experiencing. If not, feel free to open a new issue. If
you do see the same issue and are able to contribute additional information, that would be appreciated.
</p>
<p>
If you are unable to use this tool, then the manual installation methods should still work.
</p>
`},espSerialConnect:{closeable:!0,template:e=>html`
<p>
Make sure your board is plugged into this computer via a Serial connection using a USB Cable.
</p>
<ul>
<li><em><strong>NOTE:</strong> A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.</em></li>
</ul>
<p>
<button id="butConnect" type="button" @click=${this.espToolConnectHandler.bind(this)}>Connect</button>
Click this button to open the Web Serial connection menu.
</p>
<p>There may be many devices listed, such as your remembered Bluetooth peripherals, anything else plugged into USB, etc.</p>
<p>
If you aren't sure which to choose, look for words like "USB", "UART", "JTAG", and "Bridge Controller". There may be more than one right option depending on your system configuration. Experiment if needed.
</p>
`,buttons:[this.previousButton,{label:"Next",onClick:this.nextStep,isEnabled:async()=>this.currentStep<this.currentFlow.steps.length-1&&this.connected==this.connectionStates.CONNECTED,onUpdate:async e=>{this.currentDialogElement.querySelector("#butConnect").innerText=this.connected}}]},confirm:{template:e=>html`
<p>This will overwrite everything on the ${e.boardName}.</p>
`,buttons:[this.previousButton,{label:"Continue",onClick:this.nextStep}]},bootDriveSelect:{closeable:!0,template:e=>html`
<p>
Please select the ${e.drivename} Drive where the UF2 file will be copied.
</p>
<p>
If you just installed the bootloader, you may need to reset your board. If you already had the bootloader installed,
you may need to double press the reset button.
</p>
<p>
<button id="butSelectBootDrive" type="button" @click=${this.bootDriveSelectHandler.bind(this)}>Select ${e.drivename} Drive</button>
</p>
`,buttons:[]},circuitpyDriveSelect:{closeable:!0,template:e=>html`
<p>
Please select the CIRCUITPY Drive. If you don't see your CIRCUITPY drive, it may be disabled in boot.py or you may have renamed it at some point.
</p>
<p>
<button id="butSelectCpyDrive" type="button" @click=${this.circuitpyDriveSelectHandler.bind(this)}>Select CIRCUITPY Drive</button>
</p>
`,buttons:[]},actionWaiting:{template:e=>html`
<p class="centered">${e.action}...</p>
<div class="loader"><div></div><div></div><div></div><div></div></div>
`,buttons:[]},actionProgress:{template:e=>html`
<p>${e.action}...</p>
<progress id="stepProgress" max="100" value="${e.percentage}"> ${e.percentage}% </progress>
`,buttons:[]},cpSerial:{closeable:!0,template:e=>html`
<p>
The next step is to write your credentials to settings.toml. Make sure your board is running CircuitPython. <strong>If you just installed CircuitPython, you may to reset the board first.</strong>
</p>
<p>
<button id="butConnect" type="button" @click=${this.cpSerialConnectHandler.bind(this)}>Connect</button>
Click this button to open the Web Serial connection menu. If it is already connected, pressing again will allow you to select a different port.
</p>
<p>${e.serialPortInstructions}</p>
`,buttons:[this.previousButton,{label:"Next",onClick:this.nextStep,isEnabled:async()=>this.currentStep<this.currentFlow.steps.length-1&&!!this.replSerialDevice,onUpdate:async e=>{this.currentDialogElement.querySelector("#butConnect").innerText=this.replSerialDevice?"Connected":"Connect"}}]},credentials:{closeable:!0,template:e=>html`
<fieldset>
<div class="field">
<label for="circuitpy_wifi_ssid">WiFi Network Name (SSID):</label>
<input id="circuitpy_wifi_ssid" class="setting-data" type="text" placeholder="WiFi SSID" value="${e.wifi_ssid}" />
</div>
<div class="field">
<label for="circuitpy_wifi_password">WiFi Password:</label>
<input id="circuitpy_wifi_password" class="setting-data" type="password" placeholder="WiFi Password" value="${e.wifi_password}" />
</div>
<div class="field">
<label for="circuitpy_web_api_password">Web Workflow API Password:</label>
<input id="circuitpy_web_api_password" class="setting-data" type="password" placeholder="Web Workflow API Password" value="${e.api_password}" />
</div>
<div class="field">
<label for="circuitpy_web_api_port">Web Workflow API Port:</label>
<input id="circuitpy_web_api_port" class="setting-data" type="number" min="0" max="65535" placeholder="Web Workflow API Port" value="${e.api_port}" />
</div>
${!0===e.mass_storage_disabled||!1===e.mass_storage_disabled?html`<div class="field">
<label for="circuitpy_drive"><input id="circuitpy_drive" class="setting" type="checkbox" value="disabled" ${e.mass_storage_disabled?"checked":""} />Disable CIRCUITPY Drive (Required for write access)</label>
</div>`:""}
</fieldset>
`,buttons:[this.previousButton,{label:"Next",onClick:this.saveCredentials}]},success:{closeable:!0,template:e=>html`
<p>Successfully Completed</p>
${e.ip?html`<p>
You can edit files by going to <a href="http://${e.ip}/code/">http://${e.ip}/code/</a>.
</p>`:""}
`,buttons:[this.closeButton]},error:{closeable:!0,template:e=>html`
<p>Installation Error: ${e.message}</p>
`,buttons:[this.closeButton]}};getBoardName(e){return Object.keys(this.boardDefs).includes(e)?this.boardDefs[e].name:null}getBoardOptions(){var e,t=[];for(e of this.boardIds)t.push({id:e,name:this.getBoardName(e)});return t.sort((e,t)=>{e=e.name.trim().toLowerCase(),t=t.name.trim().toLowerCase();return e<t?-1:t<e?1:0}),t}async stepWelcome(){this.showDialog(this.dialogs.welcome,{boardName:this.boardName})}async stepSerialConnect(){this.showDialog(this.dialogs.espSerialConnect)}async stepConfirm(){this.showDialog(this.dialogs.confirm,{boardName:this.boardName})}async stepEraseAll(){this.showDialog(this.dialogs.actionWaiting,{action:"Erasing Flash"});try{await this.espStub.eraseFlash()}catch(e){this.errorMsg("Unable to finish erasing Flash memory. Please try again.")}await this.nextStep()}async stepFlashBin(){this.binFileUrl?(await this.downloadAndInstall(this.binFileUrl),await this.espHardReset(),await this.nextStep()):this.errorMsg("Missing bin file URL. Please make sure the installer button has this specified.")}async stepBootloader(){this.bootloaderUrl?(await this.downloadAndInstall(this.bootloaderUrl,"combined.bin",!0),await this.nextStep()):this.errorMsg("Missing bootloader file URL. Please make sure the installer button has this specified.")}async stepSelectBootDrive(){var e=await this.getBootDriveName();e&&this.logMsg("Waiting for user to select a bootloader volume named "+e),this.showDialog(this.dialogs.bootDriveSelect,{drivename:e||"Bootloader"})}async stepSelectCpyDrive(){this.logMsg("Waiting for user to select CIRCUITPY drive"),this.showDialog(this.dialogs.circuitpyDriveSelect)}async stepCopyUf2(){this.bootDriveHandle?(this.showDialog(this.dialogs.actionProgress,{action:"Copying "+this.uf2FileUrl}),await this.downloadAndCopy(this.uf2FileUrl),await this.nextStep()):this.errorMsg("No boot drive selected. stepSelectBootDrive should preceed this step.")}async stepSetupRepl(){var e=await this.getSerialPortName();let t=e?`There may be several devices listed, but look for one called something like ${e}.`:"There may be several devices listed. If you aren't sure which to choose, look for one that includes the name of your microcontroller.";this.showDialog(this.dialogs.cpSerial,{serialPortInstructions:t})}async stepCredentials(){this.tomlSettings=await this.getCurrentSettings(),console.log(this.tomlSettings);var e={wifi_ssid:this.getSetting("CIRCUITPY_WIFI_SSID"),wifi_password:this.getSetting("CIRCUITPY_WIFI_PASSWORD"),api_password:this.getSetting("CIRCUITPY_WEB_API_PASSWORD","passw0rd"),api_port:this.getSetting("CIRCUITPY_WEB_API_PORT",80)};this.hasNativeUsb(),this.showDialog(this.dialogs.credentials,e)}async stepSuccess(){let e={};this.repl&&(await this.repl.waitForPrompt(),this.currentFlow||this.currentFlow.steps.includes(this.stepCredentials))&&(e=await this.getDeviceHostInfo()),this.showDialog(this.dialogs.success,e)}async stepClose(){this.closeDialog()}async bootDriveSelectHandler(e){var t=await this.getBootDriveName();let i;try{i=await window.showDirectoryPicker({mode:"readwrite"})}catch(e){return}t&&t!=i.name?alert(`The selected drive named ${i.name} does not match the expected name of ${t}. Please select the correct drive.`):await this._verifyPermission(i)?(this.bootDriveHandle=i,await this.nextStep()):alert("Unable to write to the selected folder")}async circuitpyDriveSelectHandler(e){let t;try{t=await window.showDirectoryPicker({mode:"readwrite"})}catch(e){return}await this.getBootOut(t)?await this._verifyPermission(t)?(this.circuitpyDriveHandle=t,await this.nextStep()):alert("Unable to write to the selected folder"):alert("Expecting a folder with boot_out.txt. Please select the root folder of your CIRCUITPY drive.")}async espToolConnectHandler(e){await this.onReplDisconnected(e),await this.espDisconnect();let t;try{t=await this.espConnect({log:(...e)=>this.logMsg(...e),debug:()=>{},error:(...e)=>this.errorMsg(...e)})}catch(e){return void this.errorMsg("Unable to open Serial connection to board. Make sure the port is not already in use by another application or in another browser tab.")}try{this.updateEspConnected(this.connectionStates.CONNECTING),await t.initialize(),this.updateEspConnected(this.connectionStates.CONNECTED)}catch(e){return await t.disconnect(),this.updateEspConnected(this.connectionStates.DISCONNECTED),void this.errorMsg("Unable to connect to the board. Make sure it is in bootloader mode by holding the boot0 button when powering on and try again.")}try{this.logMsg("Connected to "+t.chipName),this.logMsg("MAC Address: "+this.formatMacAddr(t.macAddr())),FAMILY_TO_CHIP_MAP[this.chipFamily]==t.chipFamily?(this.logMsg("This chip checks out"),this.espStub=await t.runStub(),this.espStub.addEventListener("disconnect",()=>{this.updateEspConnected(this.connectionStates.DISCONNECTED),this.espStub=null}),await this.setBaudRateIfChipSupports(t.chipFamily,PREFERRED_BAUDRATE),await this.nextStep()):(this.errorMsg("Oops, this is the wrong firmware for your board."),await this.espDisconnect())}catch(e){await t.disconnect(),this.updateEspConnected(this.connectionStates.DISCONNECTED),this.errorMsg("Oops, we lost connection to your board before completing the install. Please check your USB connection and click Connect again. Refresh the browser if it becomes unresponsive.")}}async onSerialReceive(e){await this.repl.onSerialReceive(e)}async cpSerialConnectHandler(e){await this.espDisconnect(),await this.onReplDisconnected(e);try{this.replSerialDevice=await navigator.serial.requestPort()}catch(e){return}try{await this.replSerialDevice.open({baudRate:ESP_ROM_BAUD})}catch(e){console.error("Error. Unable to open Serial Port. Make sure it isn't already in use in another tab or application.")}await this.setupRepl(),this.nextStep()}async setupRepl(){this.replSerialDevice&&(this.repl=new REPL,this.repl.serialTransmit=this.serialTransmit.bind(this),this.replSerialDevice.addEventListener("message",this.onSerialReceive.bind(this)),this._readLoopPromise=this._readSerialLoop().catch(async function(e){await this.onReplDisconnected()}.bind(this)),this.replSerialDevice.writable)&&(this.writer=this.replSerialDevice.writable.getWriter(),await this.writer.ready)}async onReplDisconnected(e){if(this.reader){try{await this.reader.cancel()}catch(e){}this.reader=null}if(this.writer&&(await this.writer.releaseLock(),this.writer=null),this.replSerialDevice){try{await this.replSerialDevice.close()}catch(e){}this.replSerialDevice=null}}async buttonClickHandler(e,t=!1){!(1<this.boardIds.length)||this.selectedBoardId&&t?(await this.loadBoard(this.selectedBoardId),super.buttonClickHandler(e)):(this.showDialog(this.dialogs.boardSelect,{boards:this.getBoardOptions(),default:this.selectedBoardId}),this.currentDialogElement.querySelector("#availableBoards").addEventListener("change",this.updateButtons.bind(this)))}async selectBoardHandler(e){var t=this.currentDialogElement.querySelector("#availableBoards").value;Object.keys(this.boardDefs).includes(t)&&(this.selectedBoardId=t,this.closeDialog(),this.buttonClickHandler(null,!0))}async getBootDriveName(){return this._bootDriveName||await this.extractBootloaderInfo(),this._bootDriveName}async getSerialPortName(){return this._serialPortName||await this.extractBootloaderInfo(),this._serialPortName}async _verifyPermission(e){var t={mode:"readwrite"};return"granted"===await e.queryPermission(t)||"granted"===await e.requestPermission(t)}async extractBootloaderInfo(){if(!this.bootloaderUrl)return!1;var[,e]=await this.downloadAndExtract(this.bootloaderUrl,"tinyuf2.bin"),e=await e.text();let t=e.match(/B\x00B\x00([A-Z0-9\x00]{11})FAT16/);t&&2<=t.length&&(this._bootDriveName=t[1].replace(/\0/g,"")),(t=e.match(/0123456789ABCDEF(.+)\x00UF2/))&&2<=t.length&&(this._serialPortName=t[1].replace(/\0/g," ")),this.removeCachedFile(this.bootloaderUrl.split("/").pop())}async getBootOut(e){return this.readFile("boot_out.txt",e)}async readFile(e,t=null){if(!(t=t||this.circuitpyDriveHandle))return console.warn("CIRCUITPY Drive not selected and no Directory Handle provided"),null;try{return await(await(await t.getFileHandle(e)).getFile()).text()}catch(e){return null}}async writeFile(e,t,i=null){if(!(i=i||this.circuitpyDriveHandle))return console.warn("CIRCUITPY Drive not selected and no Directory Handle provided"),null;i=await(await i.getFileHandle(e,{create:!0})).createWritable();await i.write(t),await i.close()}addCachedFile(e,t){this.fileCache.push({filename:e,blob:t})}getCachedFile(e){for(var t of this.fileCache)if(t.filename===e)return t.contents;return null}removeCachedFile(e){for(var t of this.fileCache)t.filename===e&&this.fileCache.splice(this.fileCache.indexOf(t),1)}async downloadFile(t,e){let i;try{i=await fetch(t)}catch(e){return this.errorMsg("Unable to download file: "+t),null}var s=i.body.getReader(),a=+i.headers.get("Content-Length");let r=0;for(var o=[];;){var{done:l,value:n}=await s.read();if(l)break;o.push(n),r+=n.length,e.value=Math.round(r/a*100),this.logMsg(`Received ${r} of `+a)}var h,c=new Uint8Array(r);let d=0;for(h of o)c.set(h,d),d+=h.length;return new Blob([c])}async downloadAndExtract(e,t=null,i=!1){let s=e.split("/").pop(),a=this.getCachedFile(s);var r;if(a||(this.showDialog(this.dialogs.actionProgress,{action:"Downloading "+s}),r=this.currentDialogElement.querySelector("#stepProgress"),a=await this.downloadFile(e,r),i&&this.addCachedFile(s,a)),s.endsWith(".zip")&&t){if(this.showDialog(this.dialogs.actionProgress,{action:"Extracting "+t}),[e,a]=await this.findAndExtractFromZip(a,t),!a)return void this.errorMsg(`Unable to find ${t} in `+s);s=e}return[s,a]}async downloadAndInstall(t,e=null,i=!1){var[t,e]=await this.downloadAndExtract(t,e,i);if(e){i=new Uint8Array(await e.arrayBuffer()).buffer;let s=0;this.showDialog(this.dialogs.actionProgress,{action:"Flashing "+t});const a=this.currentDialogElement.querySelector("#stepProgress");a.value=0;try{await this.espStub.flashData(i,(e,t)=>{var i=Math.round(e/t*100);i>s&&(a.value=i,this.logMsg(i+`% (${e}/${t})...`),s=i)},0,0)}catch(e){this.errorMsg(`Unable to flash file: ${t}. Error Message: `+e)}}}async downloadAndCopy(t,i=null){if(i=i||this.bootDriveHandle){var s,a=this.currentDialogElement.querySelector("#stepProgress"),[t,r]=(a.value=0,await this.downloadAndExtract(t)),o=await(await i.getFileHandle(t,{create:!0})).createWritable(),l=r.size;let e=0;for(;e<l;)s=r.slice(e,e+COPY_CHUNK_SIZE),await o.write(s,{position:e,size:s.size}),e+=s.size,a.value=Math.round(e/l*100),this.logMsg(`${Math.round(e/l*100)}% (${e} / ${l}) written...`);this.logMsg("File successfully written");try{await o.close(),this.logMsg("File successfully closed")}catch(e){this.logMsg("Error closing file, probably due to board reset. Continuing...")}}else this.errorMsg("No drive handle available")}async findAndExtractFromZip(e,t){var i;for(const s of await new zip.ZipReader(new zip.BlobReader(e)).getEntries())if(0===s.filename.localeCompare(t))return i=await s.getData(new zip.BlobWriter),[s.filename,i];return[null,null]}async saveCredentials(){this.saveSetting("CIRCUITPY_WIFI_SSID"),this.saveSetting("CIRCUITPY_WIFI_PASSWORD"),this.saveSetting("CIRCUITPY_WEB_API_PASSWORD"),this.saveSetting("CIRCUITPY_WEB_API_PORT"),await this.writeSettings(this.tomlSettings),this.hasNativeUsb(),await this.nextStep()}getSetting(e,t=""){return this.tomlSettings&&this.tomlSettings.hasOwnProperty(e)?this.tomlSettings[e]:t}async getBootDisabled(){let e;if(this.repl)return!0;if(this.circuitpyDriveHandle){if(e=await this.readFile("boot.py"))return toml.parse(e);this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing...")}else this.errorMsg("Connect to the CIRCUITPY drive or the REPL first");return{}}saveBootDisabled(e){var t=this.currentDialogElement.querySelector("#circuitpy_drive");t&&(t.checked?this.tomlSettings.CIRCUITPY_DRIVE="disabled":this.tomlSettings.CIRCUITPY_DRIVE="enabled")}saveSetting(e){var t=this.currentDialogElement.querySelector("#"+e.toLowerCase());t?"number"==t.type?this.tomlSettings[e]=parseInt(t.value):"text"==t.type||"password"==t.type?this.tomlSettings[e]=t.value:this.errorMsg(`A setting was found, but a form element of type ${t.type} was not expected.`):this.errorMsg(`A setting named '${e}' was not found.`)}async runCode(e,t=!0){Array.isArray(e)&&(e=e.join("\n")),this.repl&&(e=await this.repl.runCode(e),t)&&console.log(e)}async writeSettings(e){if(this.repl){await this.runCode("import storage"),await this.runCode('storage.remount("/", False)'),await this.runCode("f = open('settings.toml', 'w')");for(var[t,i]of Object.entries(e))"string"==typeof i?await this.runCode(`f.write('${t} = "${i}"\\n')`):await this.runCode(`f.write('${t} = ${i}\\n')`);await this.runCode("f.close()"),this.showDialog(this.dialogs.actionWaiting,{action:"Waiting for IP Address..."}),await this.repl.softRestart();try{await this.timeout(async()=>{let e={};for(;0==Object.entries(e).length||null===e.ip;)e=await this.getDeviceHostInfo(),await this.sleep(300)},1e4)}catch(e){return console.warn("Unable to get IP Address. Network Credentials may be incorrect"),null}}else{if(!this.circuitpyDriveHandle)return this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"),null;e=toml.stringify(e);await this.writeFile("settings.toml",e)}}async getCurrentSettings(){let e;if(this.repl)e=await this.runCode(["f = open('settings.toml', 'r')","print(f.read())","f.close()"]);else{if(!this.circuitpyDriveHandle)return this.errorMsg("Connect to the CIRCUITPY drive or the REPL first"),{};e=await this.readFile("settings.toml")}return e?toml.parse(e):(this.logMsg("Unable to read settings.toml from CircuitPython. It may not exist. Continuing..."),{})}async espDisconnect(){this.espStub&&(await this.espStub.disconnect(),this.espStub.removeEventListener("disconnect",this.espDisconnect.bind(this)),this.updateEspConnected(this.connectionStates.DISCONNECTED),this.espStub=null),this.port&&(await this.port.close(),this.port=null)}async serialTransmit(e){var t=new TextEncoder;this.writer&&(t=t.encode(e),await this.writer.ready.catch(e=>{this.errorMsg("Ready error: "+e)}),await this.writer.write(t).catch(e=>{this.errorMsg("Chunk error: "+e)}),await this.writer.ready)}async _readSerialLoop(){if(this.replSerialDevice){var e=new Event("message"),t=new TextDecoder;if(this.replSerialDevice.readable)for(this.reader=this.replSerialDevice.readable.getReader();;){var{value:i,done:s}=await this.reader.read();if(i&&(e.data=t.decode(i),this.replSerialDevice.dispatchEvent(e)),s){this.reader.releaseLock(),await this.onReplDisconnected();break}}this.logMsg("Read Loop Stopped. Closing Serial Port.")}}async getDeviceHostInfo(){return this.repl?{ip:this.repl.getIpAddress(),version:this.repl.getVersion()}:{}}hasNativeUsb(){return!(!this.chipFamily||"esp32c3".includes(this.chipFamily))}sleep(t){return new Promise(e=>setTimeout(e,t))}timeout(e,t){return Promise.race([e(),this.sleep(t).then(()=>{throw Error("Timed Out")})])}}customElements.define("cp-install-button",CPInstallButton,{extends:"button"});export{CPInstallButton};