Skip to content

Commit

Permalink
feat: safe boot, busy tags, better connections
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobrosenberg committed Apr 28, 2022
1 parent 0dfd830 commit 22ea26e
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 52 deletions.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@
"command": "pymakr.listDevices",
"title": "List devices"
},
{
"command": "pymakr.safeBootDevice",
"title": "Safe boot device"
},
{
"command": "pymakr.resetDevice",
"title": "Hard reset device"
Expand Down Expand Up @@ -380,6 +384,11 @@
}
],
"pymakr.projectDeviceMenu": [
{
"command": "pymakr.safeBootDevice",
"when": "viewItem == connectedProjectDevice || viewItem == connectedDevice",
"group": "1-primary"
},
{
"command": "pymakr.resetDevice",
"when": "viewItem == connectedProjectDevice || viewItem == connectedDevice",
Expand Down Expand Up @@ -545,6 +554,7 @@
"@serialport/bindings-cpp": "^10.7.0",
"cheap-watch": "^1.0.4",
"consolite": "^0.3.8",
"hookar": "^0.0.7-0",
"micropython-ctl-cont": "^1.13.9",
"picomatch": "^2.3.1",
"prompts": "^2.4.2",
Expand Down
87 changes: 69 additions & 18 deletions src/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ const {
waitFor,
cherryPick,
getNearestPymakrConfig,
getNearestPymakrProjectDir,
createIsIncluded,
serializeKeyValuePairs,
} = require("./utils/misc");
const { writable } = require("./utils/store");
const { StateManager } = require("./utils/StateManager");
const picomatch = require("picomatch");
const { msgs } = require("./utils/msgs");
const { createSequenceHooksCollection } = require("hookar");

/**
* @typedef {Object} DeviceConfig
Expand All @@ -36,25 +35,26 @@ const runScriptDefaults = {
disableDedent: true,
broadcastOutputAsTerminalData: true,
runGcCollectBeforeCommand: true,
resolveBeforeResult: true,
resolveBeforeResult: false,
};

class Device {
__connectingPromise = null;

/**
* All devices are instances of this class
* @param {PyMakr} pymakr
* @param {DeviceInput} deviceInput
*/
constructor(pymakr, deviceInput) {
const { subscribe, set } = writable(this);
this.busy = writable(false, { lazy: true });
this.onTerminalData = createSequenceHooksCollection("");
this.subscribe = subscribe;
/** call whenever device changes need to be onChanged to subscriptions */
this.changed = () => set(this);
const { address, name, protocol, raw, password, id } = deviceInput;
this.id = id || `${protocol}://${address}`;

this.__connectingPromise = null;
this.pymakr = pymakr;
this.protocol = protocol;
this.address = address;
Expand Down Expand Up @@ -82,6 +82,8 @@ class Device {
if (!this.config.hidden) this.updateConnection();
subscribe(() => this.onChanged());
this.pymakr.config.subscribe(() => this.updateHideStatus());

this.busy.subscribe((val) => this.log.info(`Device: "${this.name}" is ${val ? "busy" : "idle"}`));
}

/**
Expand All @@ -104,11 +106,35 @@ class Device {
}

/**
* Proxies data from this.adapter.onTerminalData
* Can be wrapped and extended
* @param {string} data
* traces terminal data till pattern is found
* @param {string|RegExp} pattern
*/
onTerminalData(data) {}
readUntil(pattern = /^\r\n>>> $/) {
return new Promise((resolve) => {
const unsub = this.onTerminalData((str) => {
if (str.match(pattern)) {
resolve();
unsub();
}
});
});
}

safeBoot() {
// resetting the device should also reset the waiting calls
this.log.info("safe booting...");
this.adapter.__proxyMeta.reset();
this.busy.set(true);
this.adapter.sendData("\x06");
return new Promise((resolve) => {
// store is lazy, so next value can't be `true`
this.busy.next(() => {
this.log.info("safe booting complete!");
resolve()
});

});
}

/**
* Server.js will reactively assign this callback to the currently active terminal
Expand Down Expand Up @@ -144,24 +170,28 @@ class Device {
options = Object.assign({}, runScriptDefaults, options);

this.log.debugShort(`runScript:\n\n${script}\n\n`);
return this.adapter.runScript(script + "\n\r\n\r\n", options);
this.busy.set(true);
const result = await this.adapter.runScript(script + "\n\r\n\r\n", options);
this.busy.set(false);
return result;
}

/**
* Creates a MicroPythonDevice
*/
createAdapter() {
const rawAdapter = new MicroPythonDevice();

// We need to wrap the rawAdapter in a blocking proxy to make sure commands
// run in sequence rather in in parallel. See JSDoc comment for more info.
const adapter = createBlockingProxy(rawAdapter, {
exceptions: ["sendData", "reset", "connectSerial"],
beforeEachCall: () => this.connect(),
});

adapter.onTerminalData = (data) => {
rawAdapter.onTerminalData = (data) => {
this.__onTerminalDataExclusive(data);
this.onTerminalData(data);
this.onTerminalData.run(data);
this.terminalLogFile.write(data);
};

Expand Down Expand Up @@ -206,7 +236,7 @@ class Device {

// todo should be handleConnecting, handleFailedConnect and handleDisconnect
_onConnectingHandler() {
this.log.info("connecting...");
this.log.info(`connecting to "${this.name}"...`);
this.connecting = true;
}

Expand All @@ -222,6 +252,7 @@ class Device {
_onDisconnected() {
this.connected = false;
this.lostConnection = false;
this.busy.set(false);
this.changed();
}

Expand All @@ -232,11 +263,30 @@ class Device {
this.connecting = false;
this.lostConnection = false;
this.changed();
const boardInfoPromise = this.adapter.getBoardInfo();
// move getBoardInfo to front of queue and start the proxy
this.adapter.__proxyMeta.shiftLastToFront().run();
this.info = await waitFor(boardInfoPromise, 10000, msgs.boardInfoTimedOutErr(this.adapter));
this.log.debug("boardInfo", this.info);

// let the user know if this device is busy
this.updateIdleStatus();

// start the proxy queue or all calls will be left hanging
this.adapter.__proxyMeta.run();
}

/**
* Idle checker. Optimized for performance, not readability.
*/
async updateIdleStatus() {
this.busy.set(true);
let lastLine = "";
this.onTerminalData((newString) => {
const breakAt = newString.lastIndexOf("\n");
lastLine = breakAt > -1 ? newString.substring(breakAt + 1) : lastLine + newString;

const isBusy = !lastLine.match(/^>>> /);
this.busy.set(isBusy);
});

// check if we can connect
this.adapter.sendData("\r\x02\x02");
}

/** @private */
Expand All @@ -247,6 +297,7 @@ class Device {

async disconnect() {
if (this.connected) {
this.adapter.__proxyMeta.reset();
await waitFor(this.adapter.disconnect(), 2000, "Timed out while disconnecting.");
this._onDisconnected();
}
Expand Down
63 changes: 49 additions & 14 deletions src/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,36 @@ class Commands {
let doc = await vscode.workspace.openTextDocument(uri); // calls back into the provider
await vscode.window.showTextDocument(doc, { preview: false });
},

/**
* Safe boots device. Starts device without running scripts
* @param {DeviceTreeItem} treeItem
*/
safeBootDevice: async ({ device }) => {
vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress) => {
progress.report({ message: "Safe booting" });
await device.safeBoot();
});
},

/**
* Reboot device
* @param {DeviceTreeItem} treeItem
*/
resetDevice: async ({ device }) => {
// resetting the device should also reset the waiting calls
device.adapter.__proxyMeta.clearQueue();
// we don't want a stalled call to block the device
device.adapter.__proxyMeta.skipCurrent();
device.adapter.__proxyMeta.reset();
device.adapter.__proxyMeta.target.reset({ broadcastOutputAsTerminalData: true, softReset: false });
},

/**
* Soft reboot device
* @param {DeviceTreeItem} treeItem
*/
softResetDevice: async ({ device }) => {
console.log("soft reset");
device.adapter.reset({ broadcastOutputAsTerminalData: true, softReset: true });
},

/**
* Erases device and prompts for choice of template
* @param {DeviceTreeItem} treeItem
Expand All @@ -80,6 +91,7 @@ class Commands {
const picked = await vscode.window.showQuickPick(picks, { title: "How would you like to provision your device" });
if (picked) return this.commands.eraseDevice({ device }, picked._path);
},

/**
* Erases device and applies specified template
* @param {Partial<DeviceTreeItem>} treeItem
Expand Down Expand Up @@ -297,15 +309,21 @@ class Commands {
/** @type {import("micropython-ctl-cont/dist-node/src/main").RunScriptOptions} */
const options = {};

vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress) => {
progress.report({ message: `Run script on ${device.name}` });
try {
return await device.runScript(text, options);
} catch (err) {
console.log("er,", err.message);
vscode.window.showErrorMessage("Could not run script. Reason: " + err);
vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification },
async (progress) => {
progress.report({ message: `Run script on ${device.name}` })
setTimeout(() => progress.report({ message: "Closing popup in 5s. Script will continue in background." }), 5000);
try {
const scriptPromise = device.runScript(text, options);
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 10000));
return Promise.race([scriptPromise, timeoutPromise]);
} catch (err) {
console.log("er,", err.message);
vscode.window.showErrorMessage("Could not run script. Reason: " + err);
}
}
});
);
},
/**
* Calls runScriptPrompt with with the content of the selected file
Expand All @@ -319,9 +337,25 @@ class Commands {
* Connects a device
* @param {ProjectDeviceTreeItem} treeItem
*/
connect: ({ device }) => {
device.connect();
connect: async ({ device }) => {
await device.connect();
setTimeout(() => this.commands.handleBusyDevice(device), 2000);
},

/**
* @param {Device} device
*/
handleBusyDevice: async (device) => {
if (device.busy.get()) {
const options = { restart: "Restart in safe mode" };
const answer = await vscode.window.showInformationMessage(
`${device.name} seems to be busy. Do you wish restart it in safe mode?`,
options.restart
);
if (answer === options.restart) device.adapter.sendData("\x06");
}
},

/**
* Disconnects a device
* @param {ProjectDeviceTreeItem} treeItem
Expand Down Expand Up @@ -351,6 +385,7 @@ class Commands {
} else existingTerminal.term.show();
} else {
this.pymakr.terminalsStore.create(device);
this.commands.handleBusyDevice(device);
}
},

Expand Down
12 changes: 9 additions & 3 deletions src/providers/DevicesProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DevicesProvider {
return this.PyMakr.devicesStore
.get()
.filter((device) => !device.config.hidden)
.map((device) => new DeviceTreeItem(device));
.map((device) => new DeviceTreeItem(device, this));
}
return element.children;
}
Expand All @@ -39,16 +39,22 @@ class DeviceTreeItem extends vscode.TreeItem {

/**
* @param {import('../Device').Device} device
* @param {DevicesProvider} tree
*/
constructor(device) {
super(device.name, vscode.TreeItemCollapsibleState.None);
constructor(device, tree) {
super(device.name + (device.busy.get() ? " [BUSY]" : ""), vscode.TreeItemCollapsibleState.None);
this.contextValue = device.connected ? "connectedDevice" : "device";
this.device = device;
const filename = device.connected ? "lightning.svg" : "lightning-muted.svg";
this.iconPath = {
dark: path.join(__dirname + "..", "..", "..", "media", "dark", filename),
light: path.join(__dirname + "..", "..", "..", "media", "light", filename),
};
device.busy.subscribe((isBusy) =>
setTimeout(() => {
if (device.busy.get() === isBusy) tree.refresh();
}, 100)
);
}
}

Expand Down
Loading

0 comments on commit 22ea26e

Please sign in to comment.