diff --git a/README.md b/README.md index 714e4b3..aba7ca4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ LittleFS uploader compatible with Arduino IDE 2.2.1 or higher. For use with the ## Usage -`[Ctrl]+[Shift]+[P]`, then "`Upload LittleFS to Pico/ESP8266`" +`[Ctrl]` + `[Shift]` + `[P]`, then "`Upload LittleFS to Pico/ESP8266`" ## Glitches diff --git a/arduino-littlefs-upload-1.0.0.vsix b/arduino-littlefs-upload-1.0.0.vsix index 049aaaf..3309b04 100644 Binary files a/arduino-littlefs-upload-1.0.0.vsix and b/arduino-littlefs-upload-1.0.0.vsix differ diff --git a/src/extension.ts b/src/extension.ts index c1dc46e..9ef49e4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,238 +8,240 @@ const writeEmitter = new vscode.EventEmitter(); let writerReady : boolean = false; function makeTerminal(title : string) { - // If it exists, move it to the front - let w = vscode.window.terminals.find( (w) => ((w.name === title) && (w.exitStatus === undefined))); - if (w !== undefined) { - w.show(false); - return; - } - // Not found, make a new terminal - const pty = { - onDidWrite: writeEmitter.event, - open: () => { writerReady = true; }, - close: () => { writerReady = false; }, - handleInput: () => {} - }; - let terminal = (vscode.window).createTerminal({name: title, pty}); - terminal.show(); + // If it exists, move it to the front + let w = vscode.window.terminals.find( (w) => ((w.name === title) && (w.exitStatus === undefined))); + if (w !== undefined) { + w.show(false); + return; + } + // Not found, make a new terminal + const pty = { + onDidWrite: writeEmitter.event, + open: () => { writerReady = true; }, + close: () => { writerReady = false; }, + handleInput: () => {} + }; + const terminal = (vscode.window).createTerminal({name: title, pty}); + terminal.show(); } function findTool(ctx: ArduinoContext, match : string) : string | undefined { - let found = false; - let ret = undefined; - if (ctx.boardDetails !== undefined) { - Object.keys(ctx.boardDetails.buildProperties).forEach( (elem) => { - if (elem.startsWith(match) && !found && (ctx.boardDetails?.buildProperties[elem] !== undefined)) { - ret = ctx.boardDetails.buildProperties[elem]; - found = true; - } - }); - } - return ret; + let found = false; + let ret = undefined; + if (ctx.boardDetails !== undefined) { + Object.keys(ctx.boardDetails.buildProperties).forEach( (elem) => { + if (elem.startsWith(match) && !found && (ctx.boardDetails?.buildProperties[elem] !== undefined)) { + ret = ctx.boardDetails.buildProperties[elem]; + found = true; + } + }); + } + return ret; +} + +// Execute a command and display it's output in the terminal +async function runCommand(exe : string, opts : any[]) { + const cmd = spawn(exe, opts); + for await (const chunk of cmd.stdout) { + writeEmitter.fire(String(chunk).replace(/\n/g, "\r\n")); + } + for await (const chunk of cmd.stderr) { + // Write stderr in red + writeEmitter.fire("\x1b[31m" + String(chunk).replace(/\n/g, "\r\n") + "\x1b[0m"); + } + // Wait until the executable finishes + let exitCode = await new Promise( (resolve, reject) => { + cmd.on('close', resolve); + }); + return exitCode; } export function activate(context: vscode.ExtensionContext) { - // Get the Arduino info extension loaded - const arduinoContext: ArduinoContext = vscode.extensions.getExtension('dankeboy36.vscode-arduino-api')?.exports; - if (!arduinoContext) { - // Failed to load the Arduino API. - vscode.window.showErrorMessage("Unable to load the Arduino IDE Context extension."); - return; - } + // Get the Arduino info extension loaded + const arduinoContext: ArduinoContext = vscode.extensions.getExtension('dankeboy36.vscode-arduino-api')?.exports; + if (!arduinoContext) { + // Failed to load the Arduino API. + vscode.window.showErrorMessage("Unable to load the Arduino IDE Context extension."); + return; + } - // Register the command - const disposable = vscode.commands.registerCommand('arduino-littlefs-upload.uploadLittleFS', async () => { + // Register the command + const disposable = vscode.commands.registerCommand('arduino-littlefs-upload.uploadLittleFS', async () => { - //let str = JSON.stringify(arduinoContext, null, 4); - //console.log(str); + //let str = JSON.stringify(arduinoContext, null, 4); + //console.log(str); - if ((arduinoContext.boardDetails === undefined) || (arduinoContext.fqbn === undefined)){ - vscode.window.showErrorMessage("Board details not available. Compile the sketch once."); - return; - } + if ((arduinoContext.boardDetails === undefined) || (arduinoContext.fqbn === undefined)){ + vscode.window.showErrorMessage("Board details not available. Compile the sketch once."); + return; + } - makeTerminal("LittleFS Upload"); - // Wait for the terminal to become active. - while (!writerReady) { - await new Promise( resolve => setTimeout(resolve, 100) ); - } + makeTerminal("LittleFS Upload"); - // Clear the terminal - writeEmitter.fire('\x1b[2J\x1b[3J\x1b[;H'); + // Wait for the terminal to become active. + let cnt = 0; + while (!writerReady) { + if (cnt++ >= 50) { // Give it 5 seconds and then give up + vscode.window.showErrorMessage("Unable to open upload terminal"); + return; + } + await new Promise( resolve => setTimeout(resolve, 100) ); + } - writeEmitter.fire("LittleFS Filesystem Uploader\r\n\r\n"); + // Clear the terminal + writeEmitter.fire('\x1b[2J\x1b[3J\x1b[;H'); - // Need to have a data folder present, or this isn't gonna work... - let dataFolder = arduinoContext.sketchPath + "/data"; - if (!fs.existsSync(dataFolder)) { - writeEmitter.fire("ERROR: No data folder found\r\n"); - return; - } + writeEmitter.fire("LittleFS Filesystem Uploader\r\n\r\n"); - // Figure out what we're running on - let pico = false; - let esp8266 = false; - switch (arduinoContext.fqbn.split(':')[1]) { - case "rp2040": { - pico = true; - break; - } - case "esp8266": { - esp8266 = true; - break; - } - default: { - writeEmitter.fire("ERROR: Only Arduino-Pico RP2040 and ESP8266 supported.\r\n"); - return; - } - } + // Need to have a data folder present, or this isn't gonna work... + let dataFolder = arduinoContext.sketchPath + "/data"; + if (!fs.existsSync(dataFolder)) { + writeEmitter.fire("ERROR: No data folder found\r\n"); + return; + } - // Need to find the selected menu item, then get the associated build values for the FS configuration - let fsStart = 0; - let fsEnd = 0; - let page = 0; - let blocksize = 0; - let uploadSpeed = 115200; // ESP8266-only - arduinoContext.boardDetails.configOptions.forEach( (opt) => { - let optSeek = pico ? "flash" : "eesz"; - let startMarker = pico ? "fs_start" : "spiffs_start"; - let endMarker = pico ? "fs_end" : "spiffs_end"; - if (String(opt.option) === String(optSeek)) { - opt.values.forEach( (itm) => { - if (itm.selected) { - let menustr = "menu." + optSeek + "." + itm.value + ".build."; - fsStart = Number(arduinoContext.boardDetails?.buildProperties[menustr + startMarker]); - fsEnd = Number(arduinoContext.boardDetails?.buildProperties[menustr + endMarker]); - if (pico) { // Fixed-size always - page = 256; - blocksize = 4096; - } else if (esp8266) { - page = Number(arduinoContext.boardDetails?.buildProperties[menustr + "spiffs_pagesize"]); - blocksize = Number(arduinoContext.boardDetails?.buildProperties[menustr + "spiffs_blocksize"]); - } - } - }); - } else if (String(opt.option) === "baud") { - opt.values.forEach( (itm) => { - if (itm.selected) { - uploadSpeed = Number(itm.value); - } - }); - } - }); - if (!fsStart || !fsEnd || !page || !blocksize || (fsEnd <= fsStart)) { - writeEmitter.fire("ERROR: No filesystem specified, check flash size menu\r\n"); - return; - } + // Figure out what we're running on + let pico = false; + let esp8266 = false; + switch (arduinoContext.fqbn.split(':')[1]) { + case "rp2040": { + pico = true; + break; + } + case "esp8266": { + esp8266 = true; + break; + } + default: { + writeEmitter.fire("ERROR: Only Arduino-Pico RP2040 and ESP8266 supported.\r\n"); + return; + } + } - // Windows exes need ".exe" suffix - let ext = (platform() === 'win32') ? ".exe" : ""; - let mklittlefs = "mklittlefs" + ext; + // Need to find the selected menu item, then get the associated build values for the FS configuration + let fsStart = 0; + let fsEnd = 0; + let page = 0; + let blocksize = 0; + let uploadSpeed = 115200; // ESP8266-only + arduinoContext.boardDetails.configOptions.forEach( (opt) => { + let optSeek = pico ? "flash" : "eesz"; + let startMarker = pico ? "fs_start" : "spiffs_start"; + let endMarker = pico ? "fs_end" : "spiffs_end"; + if (String(opt.option) === String(optSeek)) { + opt.values.forEach( (itm) => { + if (itm.selected) { + let menustr = "menu." + optSeek + "." + itm.value + ".build."; + fsStart = Number(arduinoContext.boardDetails?.buildProperties[menustr + startMarker]); + fsEnd = Number(arduinoContext.boardDetails?.buildProperties[menustr + endMarker]); + if (pico) { // Fixed-size always + page = 256; + blocksize = 4096; + } else if (esp8266) { + page = Number(arduinoContext.boardDetails?.buildProperties[menustr + "spiffs_pagesize"]); + blocksize = Number(arduinoContext.boardDetails?.buildProperties[menustr + "spiffs_blocksize"]); + } + } + }); + } else if (String(opt.option) === "baud") { + opt.values.forEach( (itm) => { + if (itm.selected) { + uploadSpeed = Number(itm.value); + } + }); + } + }); + if (!fsStart || !fsEnd || !page || !blocksize || (fsEnd <= fsStart)) { + writeEmitter.fire("ERROR: No filesystem specified, check flash size menu\r\n"); + return; + } - let tool = undefined; - if (pico) { - tool = findTool(arduinoContext, "runtime.tools.pqt-mklittlefs"); - } else { // ESP826 - tool = findTool(arduinoContext, "runtime.tools.mklittlefs"); - } - if (tool) { - mklittlefs = tool + "/" + mklittlefs; - } + // Windows exes need ".exe" suffix + let ext = (platform() === 'win32') ? ".exe" : ""; + let mklittlefs = "mklittlefs" + ext; - // TBD - add non-serial UF2 upload via OpenOCD - let serialPort = ""; - if (arduinoContext.port?.address === undefined) { - writeEmitter.fire("ERROR: No port specified, check IDE menus.\r\n"); - return; - } else { - serialPort = arduinoContext.port?.address; - } - if (arduinoContext.port?.protocol !== "serial") { - writeEmitter.fire("ERROR: Only serial port upload supported at this time.\r\n"); - return; - } + let tool = undefined; + if (pico) { + tool = findTool(arduinoContext, "runtime.tools.pqt-mklittlefs"); + } else { // ESP8266 + tool = findTool(arduinoContext, "runtime.tools.mklittlefs"); + } + if (tool) { + mklittlefs = tool + "/" + mklittlefs; + } - let python3 = "python3" + ext; - let python3Path = undefined; - if (pico) { - python3Path = findTool(arduinoContext, "runtime.tools.pqt-python3"); - } else if (esp8266) { - python3Path = findTool(arduinoContext, "runtime.tools.python3"); - } - if (python3Path) { - python3 = python3Path + "/" + python3; - } + // TBD - add non-serial UF2 upload via OpenOCD + let serialPort = ""; + if (arduinoContext.port?.address === undefined) { + writeEmitter.fire("ERROR: No port specified, check IDE menus.\r\n"); + return; + } else { + serialPort = arduinoContext.port?.address; + } + if (arduinoContext.port?.protocol !== "serial") { + writeEmitter.fire("ERROR: Only serial port upload supported at this time.\r\n"); + return; + } - // We can't always know where the compile path is, so just use a temp name - const tmp = require('tmp'); - tmp.setGracefulCleanup(); - let imageFile = tmp.tmpNameSync({postfix: ".littlefs.bin"}); + let python3 = "python3" + ext; + let python3Path = undefined; + if (pico) { + python3Path = findTool(arduinoContext, "runtime.tools.pqt-python3"); + } else if (esp8266) { + python3Path = findTool(arduinoContext, "runtime.tools.python3"); + } + if (python3Path) { + python3 = python3Path + "/" + python3; + } - let buildOpts = ["-c", dataFolder, "-p", String(page), "-b", String(blocksize), "-s", String(fsEnd - fsStart), imageFile]; + // We can't always know where the compile path is, so just use a temp name + const tmp = require('tmp'); + tmp.setGracefulCleanup(); + let imageFile = tmp.tmpNameSync({postfix: ".littlefs.bin"}); - // All mklittlefs take the same options, so run in common - writeEmitter.fire("Building LittleFS filesystem\r\n"); - writeEmitter.fire(mklittlefs + " " + buildOpts.join(" ") + "\r\n"); + let buildOpts = ["-c", dataFolder, "-p", String(page), "-b", String(blocksize), "-s", String(fsEnd - fsStart), imageFile]; - const mkfs = spawn(mklittlefs, buildOpts); - for await (const chunk of mkfs.stdout) { - writeEmitter.fire(String(chunk).replace(/\n/g, "\r\n")); - } - for await (const chunk of mkfs.stderr) { - writeEmitter.fire(String(chunk).replace(/\n/g, "\r\n")); - } - // Wait until the executable finishes - let exitCode = await new Promise( (resolve, reject) => { - mkfs.on('close', resolve); - }); + // All mklittlefs take the same options, so run in common + writeEmitter.fire("Building LittleFS filesystem\r\n"); + writeEmitter.fire(mklittlefs + " " + buildOpts.join(" ") + "\r\n"); - if (exitCode) { - writeEmitter.fire("ERROR: Mklittlefs failed, error code: " + String(exitCode) + "\r\n\r\n"); - return; - } + let exitCode = await runCommand(mklittlefs, buildOpts); + if (exitCode) { + writeEmitter.fire("ERROR: Mklittlefs failed, error code: " + String(exitCode) + "\r\n\r\n"); + return; + } - // Upload stage differs per core - let uploadOpts : any[] = []; - if (pico) { - let uf2conv = "tools/uf2conv.py"; - let uf2Path = findTool(arduinoContext, "runtime.platform.path"); - if (uf2Path) { - uf2conv = uf2Path + "/" + uf2conv; - } - uploadOpts = [uf2conv, "--base", String(fsStart), "--serial", serialPort, "--family", "RP2040", imageFile]; - } else { - let upload = "tools/upload.py"; - let uploadPath = findTool(arduinoContext, "runtime.platform.path"); - if (uploadPath) { - upload = uploadPath + "/" + upload; - } - uploadOpts = [upload, "--chip", "esp8266", "--port", serialPort, "--baud", String(uploadSpeed), "write_flash", String(fsStart), imageFile]; - } + // Upload stage differs per core + let uploadOpts : any[] = []; + if (pico) { + let uf2conv = "tools/uf2conv.py"; + let uf2Path = findTool(arduinoContext, "runtime.platform.path"); + if (uf2Path) { + uf2conv = uf2Path + "/" + uf2conv; + } + uploadOpts = [uf2conv, "--base", String(fsStart), "--serial", serialPort, "--family", "RP2040", imageFile]; + } else { + let upload = "tools/upload.py"; + let uploadPath = findTool(arduinoContext, "runtime.platform.path"); + if (uploadPath) { + upload = uploadPath + "/" + upload; + } + uploadOpts = [upload, "--chip", "esp8266", "--port", serialPort, "--baud", String(uploadSpeed), "write_flash", String(fsStart), imageFile]; + } - writeEmitter.fire("\r\n\r\nUploading LittleFS filesystem\r\n"); - writeEmitter.fire(python3 + " " + uploadOpts.join(" ") + "\r\n"); - const upld = spawn(python3, uploadOpts); - for await (const chunk of upld.stdout) { - writeEmitter.fire(String(chunk).replace(/\n/g, "\r\n")); - } - for await (const chunk of upld.stderr) { - writeEmitter.fire(String(chunk).replace(/\n/g, "\r\n")); - } - // Wait until the executable finishes - exitCode = await new Promise( (resolve, reject) => { - upld.on('close', resolve); - }); + writeEmitter.fire("\r\n\r\nUploading LittleFS filesystem\r\n"); + writeEmitter.fire(python3 + " " + uploadOpts.join(" ") + "\r\n"); - if (exitCode) { - writeEmitter.fire("ERROR: Upload failed, error code: " + String(exitCode) + "\r\n\r\n"); - return; - } + exitCode = await runCommand(python3, uploadOpts); + if (exitCode) { + writeEmitter.fire("ERROR: Upload failed, error code: " + String(exitCode) + "\r\n\r\n"); + return; + } - writeEmitter.fire("\r\n\Completed upload.\r\n\r\n"); - vscode.window.showInformationMessage("LittleFS upload completed!"); - }); - context.subscriptions.push(disposable); + writeEmitter.fire("\r\n\Completed upload.\r\n\r\n"); + vscode.window.showInformationMessage("LittleFS upload completed!"); + }); + context.subscriptions.push(disposable); } -export function deactivate() { } \ No newline at end of file +export function deactivate() { }