diff --git a/client/README.md b/client/README.md index 99d5131..35e269a 100644 --- a/client/README.md +++ b/client/README.md @@ -1,15 +1,13 @@ # NodeMCU Development Tools For Visual Studio Code ## Features -* Intellisense supported for some NodeMCU modules. +* Intellisense supported for almost all NodeMCU modules. * Lua error detection supported. (It can only detect the first error because of luaparse's limitations) -* Uploading lua file to NodeMCU-ESP 8266 device supported. +* Uploading lua file on debug run to NodeMCU-ESP 8266 device supported. +* Debug console supported (evaluation of commands on debug console also supported) ## TODO's -* Improve intellisense support for modules. -* Add signature support. -* Add documentation for modules. * Support configurable baudrate, etc.. * Add more NodeMCU commands other than upload. diff --git a/client/package.json b/client/package.json index 006d937..e6da7e3 100644 --- a/client/package.json +++ b/client/package.json @@ -1,13 +1,13 @@ { "name": "vscode-nodemcu", "displayName": "NodeMcu", - "description": "Supports NodeMCU upload over serial port, lua error detection and lua optimization, intellisense", + "description": "Supports NodeMCU upload over serial port, debug console, lua error detection and lua optimization, intellisense", "author": { "name": "Furkan Duman", "email": "furkan0duman@gmail.com" }, "license": "MIT", - "version": "1.0.4", + "version": "1.0.5", "publisher": "fduman", "icon": "esp8266.gif", "repository": { @@ -20,7 +20,14 @@ "engines": { "vscode": "^0.10.10" }, + "keywords": [ + "nodemcu", + "lua", + "debuggers" + ], "categories": [ + "Languages", + "Debuggers", "Other" ], "activationEvents": [ @@ -28,12 +35,37 @@ ], "main": "./out/src/extension", "contributes": { - "commands": [ - { - "command": "nodemcu.upload", - "title": "Upload file to NodeMCU Device" - } - ] + "debuggers": [ + { + "type": "nodemcu", + "label": "NodeMCU Debug", + + "program": "./out/src/debug.js", + "runtime": "node", + + "configurationAttributes": { + "launch": { + "required": [ "program" ], + "properties": { + "program": { + "type": "string", + "description": "Absolute path to a text file.", + "default": "${file}" + } + } + } + }, + + "initialConfigurations": [ + { + "name": "NodeMCU-Debug", + "type": "nodemcu", + "request": "launch", + "program": "${file}" + } + ] + } + ] }, "scripts": { "vscode:prepublish": "node ./node_modules/vscode/bin/compile", @@ -46,6 +78,8 @@ }, "dependencies": { "vscode-languageclient": "^2.2.1", - "nodemcu-tool": "1.6.0" + "serialport": "4.0.1", + "vscode-debugprotocol": "^1.11.0", + "vscode-debugadapter": "^1.11.0" } } \ No newline at end of file diff --git a/client/src/.vscode/launch.json b/client/src/.vscode/launch.json new file mode 100644 index 0000000..8e92160 --- /dev/null +++ b/client/src/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out", + "preLaunchTask": "npm" + } + ] +} \ No newline at end of file diff --git a/client/src/debug.ts b/client/src/debug.ts new file mode 100644 index 0000000..b9c5583 --- /dev/null +++ b/client/src/debug.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import { + DebugSession, + InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Event, + Thread +} from 'vscode-debugadapter'; + +import {DebugProtocol} from 'vscode-debugprotocol'; +import {readFileSync} from 'fs'; +import {basename} from 'path'; + +import * as nodemcu from "./nodeMcuCommunication"; + +export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { + /** An absolute path to the program to debug. */ + program: string; +} + +class NodeMcuDebugSession extends DebugSession { + + // we don't support multiple threads, so we can use a hardcoded ID for the default thread + private static THREAD_ID = 1; + + // the contents (= lines) of the one and only file + private _sourceLines = new Array(); + + private _terminal: nodemcu.NodeMcuCommunicator; + + private _portclosing: boolean = false; + + /** + * Creates a new debug adapter that is used for one debug session. + * We configure the default implementation of a debug adapter here. + */ + public constructor() { + super(); + + // this debugger uses zero-based lines and columns + this.setDebuggerLinesStartAt1(false); + this.setDebuggerColumnsStartAt1(false); + } + + + /** + * The 'initialize' request is the first request called by the frontend + * to interrogate the features the debug adapter provides. + */ + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + + // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time, + // we request them early by sending an 'initializeRequest' to the frontend. + // The frontend will end the configuration sequence by calling 'configurationDone' request. + this.sendEvent(new InitializedEvent()); + + // This debug adapter implements the configurationDoneRequest. + response.body.supportsConfigurationDoneRequest = false; + + // make VS Code to use 'evaluate' when hovering over source + response.body.supportsEvaluateForHovers = false; + + // make VS Code to show a 'step back' button + response.body.supportsStepBack = false; + + this.sendResponse(response); + } + + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + + let sourceCode = readFileSync(args.program).toString(); + this._sourceLines = sourceCode.split('\n'); + + nodemcu.NodeMcuCommunicator.detectPort((error: string, ports: nodemcu.PortInformation[]) => { + + if (ports.length > 0) { + this.sendEvent(new OutputEvent(`NodeMCU device found on: ` + ports[0].comName + "\n")); + this._terminal = new nodemcu.NodeMcuCommunicator(ports[0].comName); + + this._terminal.registerOnPortDisconnect((error: string) => { + if (!this._portclosing) { + this.sendErrorResponse(response, 0, "NodeMCU device disconnected"); + this.shutdown(); + } + }); + + this._terminal.registerOnError((error: string) => { + this.sendErrorResponse(response, 0, "An error occured on NodeMCU device communication: " + error); + }); + + this._terminal.registerOnDataReceived((data: string) => { + this.sendEvent(new OutputEvent(data + "\n")); + }); + + this._terminal.registerOnPortOpen(() => { + this.sendEvent(new OutputEvent("Port opened\n")); + this.sendEvent(new OutputEvent("The file is uploading to NodeMCU device\n")); + this._terminal.uploadFile(this._sourceLines, basename(args.program)); + }); + + this._terminal.open(); + } else { + + this.sendErrorResponse(response, 0, "NodeMCU device not found"); + this.shutdown(); + } + }); + } + + protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { + if (this._terminal != null) { + this._portclosing = true; + this._terminal.close(); + } + + super.disconnectRequest(response, args); + } + + protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { + response.body = { + result: "", + variablesReference: 0 + }; + + this._terminal.write(args.expression); + + this.sendResponse(response); + } + + protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { + + // return the default thread + response.body = { + threads: [ + new Thread(NodeMcuDebugSession.THREAD_ID, "thread 1") + ] + }; + this.sendResponse(response); + } + + protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { + } + + protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + } + + protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + } + + protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { + } + + protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + } + + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + } + + protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { + } +} + +DebugSession.run(NodeMcuDebugSession); \ No newline at end of file diff --git a/client/src/extension.ts b/client/src/extension.ts index 4d0df68..cddce44 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -8,10 +8,8 @@ import * as path from 'path'; import { window, commands, workspace, Disposable, ExtensionContext } from 'vscode'; import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind } from 'vscode-languageclient'; -import * as nodemanager from "./nodeMcuManager"; export function activate(context: ExtensionContext) { - // The server is implemented in node let serverModule = context.asAbsolutePath(path.join('server', 'server.js')); // The debug options for the server @@ -38,16 +36,6 @@ export function activate(context: ExtensionContext) { // Create the language client and start the client. let disposable = new LanguageClient('Language Server Example', serverOptions, clientOptions).start(); - let command = commands.registerCommand('nodemcu.upload', () => { - if (window.activeTextEditor && window.activeTextEditor.document.languageId == "lua") { - nodemanager.findDevice((device) => { - nodemanager.uploadFile(device, window.activeTextEditor.document.fileName); - } - ); - } else{ - window.showErrorMessage("There must be an opened lua file"); - } - }); // Push the disposable to the context's subscriptions so that the // client can be deactivated on extension deactivation diff --git a/client/src/nodeMcuCommunication.ts b/client/src/nodeMcuCommunication.ts new file mode 100644 index 0000000..5081f7f --- /dev/null +++ b/client/src/nodeMcuCommunication.ts @@ -0,0 +1,96 @@ +import * as serialport from "serialport"; + +export interface PortInformation { + comName: string, + manufacturer: string, + serialNumber: string, + pnpId: string, + locationId: string, + vendorId: string, + productId: string +} + +export class NodeMcuCommunicator { + private _port: serialport.SerialPort; + private _baudrate = 9600; + + constructor(portname: string) { + let options = { + autoOpen: false, + baudRate: this._baudrate, + parser: serialport.parsers.readline("\r\n") + }; + + this._port = new serialport.SerialPort(portname, options); + } + + public static detectPort(callback: (error: string, ports: PortInformation[]) => void): void { + serialport.list((error: string, ports: serialport.portConfig[]) => { + if (error != null) { + callback(error, null); + } + else { + let filteredPorts = ports.filter((port: serialport.portConfig) => { + return port.pnpId.includes("VID_1A86") || port.pnpId.includes("VID_10C4"); + }); + + callback(error, filteredPorts); + } + }); + } + + public open(): void { + this._port.open(); + } + + public registerOnPortDisconnect(callback: (error: string) => void): void { + this._port.on("disconnect", callback); + } + + public registerOnPortClose(callback: () => void): void { + this._port.on("close", callback); + } + + public registerOnPortOpen(callback: () => void): void { + this._port.on("open", () => { + this.write("uart.setup(0,9600,8,0,1,1)"); + callback(); + }); + } + + public registerOnError(callback: (error: string) => void): void { + this._port.on("error", callback); + } + + public registerOnDataReceived(callback: (data: string) => void): void { + this._port.on("data", callback); + } + + public write(data: string) { + if (this._port.isOpen) { + this._port.write(data + "\r\n", () => { + this._port.drain(); + }); + } + } + + public uploadFile(data: string[], remoteFilename: string) { + this.write('file.remove("' + remoteFilename + '");'); + this.write('file.open("' + remoteFilename + '", "w+");'); + this.write("w=file.writeline;"); + + data.forEach(line => { + this.write("w([==[" + line + "]==]);"); + }); + + this.write('file.close();'); + + this.write("dofile('" + remoteFilename + "')"); + } + + public close(): void { + if (this._port.isOpen) { + this._port.close(); + } + } +} \ No newline at end of file diff --git a/client/src/nodeMcuManager.ts b/client/src/nodeMcuManager.ts deleted file mode 100644 index b05673e..0000000 --- a/client/src/nodeMcuManager.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as nodemcu from "nodemcu-tool"; -import { window } from "vscode"; -import * as path from "path"; - -export function uploadFile(device: string, filename: string) { - // TODO: NodeMcu's default uart settings is 115200. Need to implement baudrate change function - let connector = new nodemcu.Connector(device, 115200); - - connector.connect((err, deviceInfo) => { - if (err) { - window.showErrorMessage("NodeMCU connection error: " + err); - connector.disconnect(); - } - else { - window.showInformationMessage("NodeMCU device connected: " + deviceInfo); - - connector.upload(filename, path.basename(filename), { optimize: true }, (err, data) => { - if (err) { - window.showErrorMessage("NodeMCU upload error: " + err); - } - else { - window.showInformationMessage("NodeMCU upload has completed"); - } - - connector.disconnect(); - }, (current, total) => { - window.setStatusBarMessage("Upload completed: %" + Math.trunc(current / total * 100)); - }); - } - }); -} - -export function findDevice(cb: any): void { - let connector = new nodemcu.Connector("", 9600); - - // serialport module cannot detect vendor id on windows so we should get all com devices and filtered by pnpid - let osIsWindows = process.platform == "win32"; - - connector.deviceInfo(osIsWindows, (err, devices) => { - if (err) { - window.showErrorMessage('Serial device connection error: ' + err); - } else { - if (osIsWindows) - { - devices = devices.filter(function(device) { - return device.pnpId.toString().includes("VID_1A86") || device.pnpId.toString().includes("VID_10C4"); - }); - } - - if (devices.length == 0) { - window.showErrorMessage('No Connected Devices found'); - } else if (devices.length > 1) { - // TODO: Display selection popup - window.showErrorMessage("Many devices found. Connect only once"); - } - else { - window.showInformationMessage('Device found at: ' + devices[0].comName); - cb(devices[0].comName); - } - } - }); -} - diff --git a/client/typings.json b/client/typings.json new file mode 100644 index 0000000..b210a19 --- /dev/null +++ b/client/typings.json @@ -0,0 +1,5 @@ +{ + "globalDevDependencies": { + "serialport": "registry:dt/serialport#0.0.0+20160627065358" + } +} diff --git a/client/typings/globals/serialport/index.d.ts b/client/typings/globals/serialport/index.d.ts new file mode 100644 index 0000000..94c126e --- /dev/null +++ b/client/typings/globals/serialport/index.d.ts @@ -0,0 +1,48 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/19ccfe97a5bd392626dcc12daf148b97d048201e/serialport/serialport.d.ts +declare module 'serialport' { + module parsers { + function readline(delimiter: string):void; + function raw(emitter:any, buffer:string):void + } + + export class SerialPort { + constructor(path: string, options?: Object, callback?: (err:string) => void) + isOpen: boolean; + on(event: string, callback?: (data?:any) => void):void; + open(callback?: () => void):void; + write(buffer: any, callback?: (err:string, bytesWritten:number) => void):void + pause():void; + resume():void; + disconnected(err: Error):void; + close(callback?: () => void):void; + flush(callback?: () => void):void; + set(options: setOptions, callback: () => void):void; + drain(callback?: () => void):void; + update(options: updateOptions, callback?: () => void):void; + } + + export function list(callback: (err: string, ports:portConfig[]) => void): void; + + interface portConfig { + comName: string, + manufacturer: string, + serialNumber: string, + pnpId: string, + locationId: string, + vendorId: string, + productId: string + } + + interface setOptions { + brk?: boolean; + cts?: boolean; + dsr?: boolean; + dtr?: boolean; + rts?: boolean; + } + + interface updateOptions { + baudRate?: number + } +} \ No newline at end of file diff --git a/client/typings/globals/serialport/typings.json b/client/typings/globals/serialport/typings.json new file mode 100644 index 0000000..10c567c --- /dev/null +++ b/client/typings/globals/serialport/typings.json @@ -0,0 +1,8 @@ +{ + "resolution": "main", + "tree": { + "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/19ccfe97a5bd392626dcc12daf148b97d048201e/serialport/serialport.d.ts", + "raw": "registry:dt/serialport#0.0.0+20160627065358", + "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/19ccfe97a5bd392626dcc12daf148b97d048201e/serialport/serialport.d.ts" + } +} diff --git a/client/typings/index.d.ts b/client/typings/index.d.ts new file mode 100644 index 0000000..9177b06 --- /dev/null +++ b/client/typings/index.d.ts @@ -0,0 +1 @@ +///