diff --git a/package.json b/package.json index 76ab1104..7ef4be2c 100644 --- a/package.json +++ b/package.json @@ -518,6 +518,11 @@ "title": "Telnet", "category": "PyMakr" }, + { + "command": "pymakr.newDeviceWS", + "title": "WebSocket", + "category": "PyMakr" + }, { "command": "pymakr.setVisibleDevices", "title": "Set visible devices", @@ -559,12 +564,6 @@ "title": "Select devices", "category": "PyMakr" }, - { - "command": "pymakr.removeDeviceFromProject", - "title": "Remove device from project", - "shortTitle": "Remove device", - "category": "PyMakr" - }, { "command": "pymakr.uploadPrompt", "title": "Upload to device", @@ -710,6 +709,10 @@ "command": "pymakr.newDeviceTelnet", "when": "false" }, + { + "command": "pymakr.newDeviceWS", + "when": "false" + }, { "command": "pymakr.setVisibleDevices", "when": "false" @@ -730,10 +733,6 @@ "command": "pymakr.selectDevicesForProjectPrompt", "when": "false" }, - { - "command": "pymakr.removeDeviceFromProject", - "when": "false" - }, { "command": "pymakr.uploadPrompt", "when": "false" @@ -757,6 +756,9 @@ }, { "command": "pymakr.newDeviceTelnet" + }, + { + "command": "pymakr.newDeviceWS" } ], "pymakr.projectMenu": [ @@ -791,11 +793,6 @@ "command": "pymakr.disconnect", "group": "1-primary" }, - { - "command": "pymakr.removeDeviceFromProject", - "when": "viewItem =~ /.+#project#device/", - "group": "4-config" - }, { "command": "pymakr.eraseDevicePrompt", "group": "4-config" diff --git a/src/Device.js b/src/Device.js index d42c88be..eb671480 100644 --- a/src/Device.js +++ b/src/Device.js @@ -147,7 +147,8 @@ class Device { } get serialized() { - return serializeKeyValuePairs(this.raw); + // raw is set only for Serial connected devices + return (this.raw === undefined) ? '' : serializeKeyValuePairs(this.raw); } /** the full device name using the naming template */ @@ -272,7 +273,7 @@ class Device { // 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", "connectSerial", "getState"] }); + const adapter = createBlockingProxy(rawAdapter, { exceptions: ["sendData", "connectSerial", "connectNetwork", "getState"] }); adapter.__proxyMeta.beforeEachCall(({ item }) => { this.action.set(item.field.toString()); this.busy.set(true); @@ -343,7 +344,7 @@ class Device { const reconnectIntervals = [0, 5, 500, 1000]; while (reconnectIntervals.length) { try { - await this._connectSerial(); + await this._connect(); this.connected.set(true); resolve(this._onConnectedHandler()); return this.__connectingPromise; @@ -424,9 +425,18 @@ class Device { } /** @private */ - async _connectSerial() { - const connectPromise = this.adapter.connectSerial(this.address, this.config.adapterOptions); - await waitFor(connectPromise, 2000, "Timed out while connecting."); + async _connect() { + if (this.protocol == "serial") { + const connectPromise = this.adapter.connectSerial(this.address, this.config.adapterOptions); + await waitFor(connectPromise, 2000, "Timed out while connecting."); + } else if (this.protocol == "ws") { + const connectPromise = this.adapter.connectNetwork(this.address, this.password); + await waitFor(connectPromise, 10000, "Timed out while connecting."); + } else if (this.protocol == "telnet") { + // TODO: not handled + } else { + // unknown protocol + } } async disconnect() { diff --git a/src/PyMakr.js b/src/PyMakr.js index e8ffab30..df3e8a90 100644 --- a/src/PyMakr.js +++ b/src/PyMakr.js @@ -108,7 +108,13 @@ class PyMakr { } getTerminalProfile(protocol, address) { - const hasNode = () => require("child_process").execSync("node -v").toString().startsWith("v"); + const hasNode = () => { + try { + return require("child_process").execSync("node -v", {}).toString().startsWith("v"); + } catch (err) { + this.log.info("Node not found. Using prebuild binaries"); + } + }; const binariesPath = resolve(__dirname, "terminal/bin"); const userSelection = this.config.get().get("terminalProfile"); @@ -153,6 +159,7 @@ class PyMakr { }), vscode.workspace.registerFileSystemProvider("serial", this.fileSystemProvider, { isCaseSensitive: true }), // vscode.workspace.registerFileSystemProvider("telnet", this.fileSystemProvider, { isCaseSensitive: true }), + vscode.workspace.registerFileSystemProvider("ws", this.fileSystemProvider, { isCaseSensitive: true }), vscode.window.registerTreeDataProvider("pymakr-projects-tree", this.projectsProvider), vscode.window.registerTreeDataProvider("pymakr-devices-tree", this.devicesProvider), vscode.workspace.registerTextDocumentContentProvider("pymakrDocument", this.textDocumentProvider), @@ -182,7 +189,7 @@ class PyMakr { * Registers usb devices and scans for projects in workspace */ async setup() { - await Promise.all([this.devicesStore.registerUSBDevices(), this.registerProjects()]); + await Promise.all([this.devicesStore.registerUSBDevices(), this.devicesStore.registerWSDevices(), this.registerProjects()]); this.projectsProvider.refresh(); // tell the provider that projects were updated this.context.subscriptions.push(coerceDisposable(this.devicesStore.watchUSBDevices())); } diff --git a/src/commands/index.js b/src/commands/index.js index 635e0d78..0d3b4095 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -49,7 +49,13 @@ class Commands { * @param {{device: Device}} param0 */ "debug.showDeviceSummary": async ({ device }) => { - let uri = vscode.Uri.parse("pymakrDocument:" + "Pymakr: device summary - " + device.raw.path); + let path; + if (device.protocol === "serial") { + path = device.raw.path; + } else { + path = device.id; + } + let uri = vscode.Uri.parse("pymakrDocument:" + "Pymakr: device summary - " + path); this.pymakr.textDocumentProvider.onDidChangeEmitter.fire(uri); vscode.commands.executeCommand("markdown.showPreview", uri); }, @@ -591,6 +597,27 @@ class Commands { this.pymakr.devicesStore.upsert({ address, protocol, name, username, password }); }, + /** + * Not currently in supported + */ + newDeviceWS: async () => { + const address = await vscode.window.showInputBox({ + placeHolder: "192.168.0.x", + prompt: "Hostname or IP of your device", + }); + const password = await vscode.window.showInputBox({ + password: true, + prompt: "Password for your device [default: python]", + value: "python", + }); + const name = await vscode.window.showInputBox({ + value: `ws://${address}`, + prompt: "Name of your device", + }); + const protocol = "ws"; + this.pymakr.devicesStore.upsert({ address, protocol, name, password }); + }, + /** * Create a serial device manually */ diff --git a/src/providers/TextDocumentProvider.js b/src/providers/TextDocumentProvider.js index 5a2b69a5..ba610fac 100644 --- a/src/providers/TextDocumentProvider.js +++ b/src/providers/TextDocumentProvider.js @@ -51,10 +51,15 @@ class TextDocumentProvider { match: /device summary - (.+)/, body: (_all, path) => { const GithubMaxLengthUri = 8170; //8182 to be exact - const device = this.pymakr.devicesStore.get().find((device) => device.raw.path === path); + const device = this.pymakr.devicesStore.get().find((device) => device.raw?.path === path || device.id === path); const config = { ...device.config, password: "***", username: "***" }; const configTable = arraysToMarkdownTable([["Config", ""], ...Object.entries(config || {})]); - const deviceTable = arraysToMarkdownTable([["Device", ""], ...Object.entries(device.raw || {})]); + let deviceTable; + if (device.protocol === "serial") { + deviceTable = arraysToMarkdownTable([["Device", ""], ...Object.entries(device.raw || {})]); + } else { + deviceTable = arraysToMarkdownTable([["Device", ""], ["protocol", device.protocol]]); + } const systemTable = arraysToMarkdownTable([["System", ""], ...Object.entries(device.info || {})]); const hostTable = arraysToMarkdownTable([ ["Host", ""], diff --git a/src/stores/devices.js b/src/stores/devices.js index d2231f72..1359e18f 100644 --- a/src/stores/devices.js +++ b/src/stores/devices.js @@ -16,6 +16,21 @@ const rawSerialToDeviceInput = (raw) => ({ raw, }); +/** + * converts configuration's websocket DeviceInput into a DeviceInput + * @param {String} id + * @param {DeviceInput} diConf + * @returns {DeviceInput} + */ +const wsConfigToDeviceInput = (id, diConf) => { + const address = id.substring('ws://'.length); + const protocol = "ws"; + const name = diConf.name == undefined || diConf.name === '' ? id : diConf.name; + const password = diConf.password; + return { address, protocol, name, password, id }; +}; + + /** * @param {DeviceInput} device * @returns {string} @@ -77,6 +92,15 @@ const createDevicesStore = (pymakr) => { }); }; + const registerWSDevices = async () => { + pymakr.log.trace("register WS devices"); + const deviceConfigs = pymakr.config.get().get("devices").configs; + const deviceInputs = Object.keys(deviceConfigs) + .filter(k => k.startsWith('ws://')) // register only ws protocol + .map(k => wsConfigToDeviceInput(k, deviceConfigs[k])); + upsert(deviceInputs); + }; + /** @type {NodeJS.Timer} */ let watchIntervalHandle; const watchUSBDevices = () => { @@ -96,6 +120,7 @@ const createDevicesStore = (pymakr) => { getAllById, upsert, remove, + registerWSDevices, registerUSBDevices, watchUSBDevices, stopWatchingUSBDevices: () => clearInterval(watchIntervalHandle), diff --git a/src/terminal/Server.js b/src/terminal/Server.js index b145ecca..80dd9ecf 100644 --- a/src/terminal/Server.js +++ b/src/terminal/Server.js @@ -36,7 +36,13 @@ class Server { // listen to keystrokes from client socket.on("data", (data) => { this.log.debug("received", data.toString(), data[0]); - device.adapter.sendData(data); + if (device.protocol === "ws") { + // Terminal data should be sent as string for WS protocol + // As it is specified in webrepl README Terminal Protocol + device.adapter.sendData(data.toString()); + } else { + device.adapter.sendData(data); + } // make sure device data is sent to the last active terminal device.__onTerminalDataExclusive = (data) => socket.write(data); }); diff --git a/types/typedef.js b/types/typedef.js index e38b094c..c1a7d0cd 100644 --- a/types/typedef.js +++ b/types/typedef.js @@ -16,7 +16,7 @@ /** * @typedef {Object} DeviceInput * @prop {string} name - * @prop {'serial'|'telnet'} protocol + * @prop {'serial'|'telnet'|'ws'} protocol * @prop {string} address * @prop {string=} username * @prop {string=} password