Skip to content
This repository was archived by the owner on Oct 1, 2024. It is now read-only.

Add CLI Burn Bootloader Command #1463

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ This extension provides several commands in the Command Palette (<kbd>F1</kbd> o
- **Arduino: CLI Upload**: Upload complied code without building sketch (CLI only).
- **Arduino: Upload Using Programmer**: Upload using an external programmer.
- **Arduino: CLI Upload Using Programmer**: Upload using an external programmer without building sketch (CLI only).
- **Arduino: CLI Burn Bootloader**: Burn bootloader using external programmer (CLI Only).
- **Arduino: Verify**: Build sketch.
- **Arduino: Rebuild IntelliSense Configuration**: Forced/manual rebuild of the IntelliSense configuration. The extension analyzes Arduino's build output and sets the IntelliSense include paths, defines, compiler arguments accordingly.

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
"command": "arduino.cliUploadUsingProgrammer",
"title": "Arduino CLI: Upload Using Programmer"
},
{
"command": "arduino.cliBurnBootloader",
"title": "Arduino CLI: Burn Bootloader"
},
{
"command": "arduino.rebuildIntelliSenseConfig",
"title": "Arduino: Rebuild IntelliSense Configuration"
Expand Down
223 changes: 190 additions & 33 deletions src/arduino/arduino.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export enum BuildMode {
CliUpload = "Uploading using Arduino CLI",
UploadProgrammer = "Uploading (programmer)",
CliUploadProgrammer = "Uploading (programmer) using Arduino CLI",
CliBurnBootloader = "Burning Bootloader using Arduino CLI",
}

export enum ArduinoState {
Idle,
Building,
BurningBootloader,
}

/**
Expand All @@ -64,18 +71,18 @@ export class ArduinoApp {
private _analysisManager: AnalysisManager;

/**
* Indicates if a build is currently in progress.
* If so any call to this.build() will return false immediately.
* Indicates if a build or bootloader burn is currently in progress.
* If so any call to this.build() or this.burnBootloader() will return false immediately.
*/
private _building: boolean = false;
private _state: ArduinoState = ArduinoState.Idle;

/**
* @param {IArduinoSettings} _settings ArduinoSetting object.
*/
constructor(private _settings: IArduinoSettings) {
const analysisDelayMs = 1000 * 3;
this._analysisManager = new AnalysisManager(
() => this._building,
() => this._state !== ArduinoState.Idle,
async () => { await this.build(BuildMode.Analyze); },
analysisDelayMs);
}
Expand Down Expand Up @@ -148,10 +155,10 @@ export class ArduinoApp {
}

/**
* Returns true if a build is currently in progress.
* Returns true if a build or bootloader burn is currently in progress.
*/
public get building() {
return this._building;
public get state() {
return this._state;
}

/**
Expand All @@ -171,30 +178,54 @@ export class ArduinoApp {
* @param buildDir Override the build directory set by the project settings
* with the given directory.
* @returns true on success, false if
* * another build is currently in progress
* * another build or burn bootloader operation is currently in progress
* * board- or programmer-manager aren't initialized yet
* * or something went wrong during the build
*/
public async build(buildMode: BuildMode, buildDir?: string) {

if (!this._boardManager || !this._programmerManager || this._building) {
if (!this._boardManager || !this._programmerManager || this._state !== ArduinoState.Idle) {
return false;
}

this._building = true;
this._state = ArduinoState.Building;

return await this._build(buildMode, buildDir)
.then((ret) => {
this._building = false;
return ret;
})
.catch((reason) => {
this._building = false;
try {
return await this._build(buildMode, buildDir);
} catch (reason) {
logger.notifyUserError("ArduinoApp.build",
reason,
`Unhandled exception when cleaning up build "${buildMode}": ${JSON.stringify(reason)}`);
return false;
});
} finally {
this._state = ArduinoState.Idle;
}
}

/**
* Burns the bootloader onto the currently selected board using the currently
* selected programmer.
* @returns true on success, false if
* * another build or burn bootloader operation is currently in progress
* * board- or programmer-manager aren't initialized yet
* * something went wrong while burning the bootloader
*/
public async burnBootloader() {
if (!this._boardManager || !this.programmerManager || this._state !== ArduinoState.Idle) {
return false;
}

this._state = ArduinoState.BurningBootloader;
try {
return await this._burnBootloader();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is located in a try catch, but the _burnBootloader method seems to primarly use the boolean return value to signal success or failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I mostly followed a similar flow to build() which handles potentially unhandled exceptions.
Originally the code was like the following:

return await thenable.then((ret) => {
    // setState
    return ret;
).catch((reason) => {
    // setState
    // logReason
    return false;
});

I figured that a try catch finally approach would do the same task and appear more readable and reduce the repeated lines that set the state. The only difference being that the state is set after the operation returns.

try {
    return await thenable;
} catch (reason) {
    // logReason
    return false;
} finally {
    // setState
}

I'm open to suggestions though!

} catch (reason) {
logger.notifyUserError("ArduinoApp.burnBootloader",
reason,
`Unhandled exception burning bootloader: ${JSON.stringify(reason)}`);
return false;
} finally {
this._state = ArduinoState.Idle;
}
}

// Include the *.h header files from selected library to the arduino sketch.
Expand Down Expand Up @@ -511,9 +542,22 @@ export class ArduinoApp {
return line.startsWith("Sketch uses ") || line.startsWith("Global variables use ");
}

/**
* Triggers serial selection prompt. Used in build and burnBootloader
* processes if no serial port selected already.
*/
private async _selectSerial(): Promise<void> {
const choice = await vscode.window.showInformationMessage(
"Serial port is not specified. Do you want to select a serial port for uploading?",
"Yes", "No");
if (choice === "Yes") {
vscode.commands.executeCommand("arduino.selectSerialPort");
}
}

/**
* Private implementation. Not to be called directly. The wrapper build()
* manages the build state.
* manages the busy state.
* @param buildMode See build()
* @param buildDir See build()
* @see https://github.com/arduino/Arduino/blob/master/build/shared/manpage.adoc
Expand Down Expand Up @@ -554,18 +598,9 @@ export class ArduinoApp {
}
}

const selectSerial = async () => {
const choice = await vscode.window.showInformationMessage(
"Serial port is not specified. Do you want to select a serial port for uploading?",
"Yes", "No");
if (choice === "Yes") {
vscode.commands.executeCommand("arduino.selectSerialPort");
}
}

if (buildMode === BuildMode.Upload) {
if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) {
await selectSerial();
await this._selectSerial();
return false;
}

Expand All @@ -580,7 +615,7 @@ export class ArduinoApp {
}
} else if (buildMode === BuildMode.CliUpload) {
if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) {
await selectSerial();
await this._selectSerial();
return false;
}

Expand All @@ -601,7 +636,7 @@ export class ArduinoApp {
return false;
}
if (!dc.port) {
await selectSerial();
await this._selectSerial();
return false;
}

Expand All @@ -623,7 +658,7 @@ export class ArduinoApp {
return false;
}
if (!dc.port) {
await selectSerial();
await this._selectSerial();
return false;
}
if (!this.useArduinoCli()) {
Expand Down Expand Up @@ -665,7 +700,7 @@ export class ArduinoApp {
await vscode.workspace.saveAll(false);

// we prepare the channel here since all following code will
// or at leas can possibly output to it
// or at least can possibly output to it
arduinoChannel.show();
if (VscodeSettings.getInstance().clearOutputOnBuild) {
arduinoChannel.clear();
Expand Down Expand Up @@ -835,4 +870,126 @@ export class ArduinoApp {
return false;
});
}

/**
* Private implementation. Not to be called directly. The wrapper burnBootloader()
* manages the busy state.
* @see https://arduino.github.io/arduino-cli/
* @see https://github.com/arduino/Arduino/issues/11765
* @remarks Currently this is only supported by `arduino-cli`. A request has been
* made with the Arduino repo.
*/
private async _burnBootloader(): Promise<boolean> {
const dc = DeviceContext.getInstance();
const args: string[] = [];
let restoreSerialMonitor: boolean = false;
const verbose = VscodeSettings.getInstance().logLevel === constants.LogLevel.Verbose;

if (!this.boardManager.currentBoard) {
logger.notifyUserError("boardManager.currentBoard", new Error(constants.messages.NO_BOARD_SELECTED));
return false;
}
const boardDescriptor = this.boardManager.currentBoard.getBuildConfig();

if (this.useArduinoCli()) {
args.push("burn-bootloader",
"-b", boardDescriptor);
} else {
arduinoChannel.error("This command is only available when using the Arduino CLI");
return false;
}

if (!dc.port) {
await this._selectSerial();
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding this right, this forces someone to select the port, and then have to restart the burn bootloader process. Is this what you were going after?

Copy link
Contributor Author

@davidcooper1 davidcooper1 Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was using the same flow found in _build() for this just to be safe. I do like the idea of allowing burn bootloader or build operations continuing if a serial is selected though. It may be worth bringing this up in a separate issue.

}
args.push("--port", dc.port);

const programmer = this.programmerManager.currentProgrammer;
if (!programmer) {
logger.notifyUserError("programmerManager.currentProgrammer", new Error(constants.messages.NO_PROGRAMMMER_SELECTED));
return false;
}
args.push("--programmer", programmer);

// We always build verbosely but filter the output based on the settings
args.push("--verbose");

if (dc.buildPreferences) {
for (const pref of dc.buildPreferences) {
// Note: BuildPrefSetting makes sure that each preference
// value consists of exactly two items (key and value).
args.push("--build-property", `${pref[0]}=${pref[1]}`);
}
}

// we prepare the channel here since all following code will
// or at least can possibly output to it
arduinoChannel.show();
if (VscodeSettings.getInstance().clearOutputOnBuild) {
arduinoChannel.clear();
}
arduinoChannel.start(`Burning booloader for ${boardDescriptor} using programmer ${programmer}'`);

restoreSerialMonitor = await SerialMonitor.getInstance().closeSerialMonitor(dc.port);
UsbDetector.getInstance().pauseListening();

const cleanup = async () => {
UsbDetector.getInstance().resumeListening();
if (restoreSerialMonitor) {
await SerialMonitor.getInstance().openSerialMonitor();
}
}

const stdoutcb = (line: string) => {
if (verbose) {
arduinoChannel.channel.append(line);
}
}

const stderrcb = (line: string) => {
if (os.platform() === "win32") {
line = line.trim();
if (line.length <= 0) {
return;
}
line = line.replace(/(?:\r|\r\n|\n)+/g, os.EOL);
line = `${line}${os.EOL}`;
}
if (!verbose) {
// Don't spill log with spurious info from the backend. This
// list could be fetched from a config file to accommodate
// messages of unknown board packages, newer backend revisions
const filters = [
/^Picked\sup\sJAVA_TOOL_OPTIONS:\s+/,
/^\d+\d+-\d+-\d+T\d+:\d+:\d+.\d+Z\s(?:INFO|WARN)\s/,
/^(?:DEBUG|TRACE|INFO)\s+/,
];
for (const f of filters) {
if (line.match(f)) {
return;
}
}
}
arduinoChannel.channel.append(line);
}

return await util.spawn(
this._settings.commandPath,
args,
{ cwd: ArduinoWorkspace.rootPath },
{ /*channel: arduinoChannel.channel,*/ stdout: stdoutcb, stderr: stderrcb },
).then(async () => {
await cleanup();
arduinoChannel.end(`Burning booloader for ${boardDescriptor} using programmer ${programmer}'`);
return true;
}, async (reason) => {
await cleanup();
const msg = reason.code
? `Exit with code=${reason.code}`
: JSON.stringify(reason);
arduinoChannel.error(`Burning booloader for ${boardDescriptor} using programmer ${programmer}': ${msg}${os.EOL}`);
return false;
});
}
}
Loading