diff --git a/README.md b/README.md index d675ab0a5..5580104cc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,23 @@ # Device Simulator Express, a Microsoft Garage project +Python versions: 3.7+ Project Status: Active – The project has reached a stable, usable state and is being actively developed. License: We are using the MIT License We are welcoming PRS! Platforms Supported: Windows, MacOSX + +Azure DevOps Board Badge + Make without limit! Device Simulator Express, a Microsoft Garage project, allows you to code microcontrollers without the hardware on hand! You can program your Circuit Playground Express (CPX) or your BBC micro:bit! Test and debug your code on the device simulator and see the same result when you plug in your actual microcontroller. Curious about the output of the device, the serial monitor allows you to observe the device output. CircuitPlayground Express +## Build Status + +| Branch | Build Status | +| :------ | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| dev | [![Build Status](https://microsoftgarage.visualstudio.com/Intern%20GitHub/_apis/build/status/Adafruit/Pacifica-CI?branchName=dev)](https://microsoftgarage.visualstudio.com/Intern%20GitHub/_build/latest?definitionId=304&branchName=dev) | +| staging | [![Build Status](https://microsoftgarage.visualstudio.com/Intern%20GitHub/_apis/build/status/Adafruit/Pacifica-CI?branchName=staging)](https://microsoftgarage.visualstudio.com/Intern%20GitHub/_build/latest?definitionId=304&branchName=staging) | +| master | [![Build Status](https://microsoftgarage.visualstudio.com/Intern%20GitHub/_apis/build/status/Adafruit/Pacifica-CI?branchName=master)](https://microsoftgarage.visualstudio.com/Intern%20GitHub/_build/latest?definitionId=304&branchName=master) | + ## Prerequisites @@ -89,7 +101,6 @@ Before deploying the python code to your CPX device, you need to format your dev 1. Download the firmware with the .uf2 file (link: https://learn.adafruit.com/adafruit-circuit-playground-express/circuitpython-quickstart) 2. Download the lastest version of the cpx library (link: https://learn.adafruit.com/welcome-to-circuitpython/circuitpython-libraries). - **_Note:_** Make sure you name your file `main.py` or `code.py`: the device automatically runs the first file that is likely named. This is the convention for CircuitPython ([source](https://learn.adafruit.com/welcome-to-circuitpython/creating-and-editing-code#naming-your-program-file-7-32)). Then, if you are on Windows, you will also need to install the Python Pywin32 package. Use the following command in the console: `pip install pywin32` @@ -98,7 +109,7 @@ Then, if you are on Windows, you will also need to install the Python Pywin32 pa #### 5. Use the Serial Monitor for your Adafruit CPX device (available on Windows and Mac only) 1. Plug in your CPX device (make sure it’s formatted properly already) -2. Run the command `"Device Simulator Express: [Circuit Playground Express] Open Serial Monitor"` +2. Run the command `"Device Simulator Express: Open Serial Monitor"` 3. Select your baud rate for the serial port 4. The print() statements in your code will show in the output console @@ -123,11 +134,11 @@ Device Simulator Express provides several commands in the Command Palette (F1 or - `Device Simulator Express: [Circuit Playground Express] New File`: Opens an unsaved .py file with template code, also opens the simulator. - `Device Simulator Express: [Circuit Playground Express] Open Simulator`: Opens the simulator in the webView - `Device Simulator Express: [Circuit Playground Express] Run on Simulator`: Runs python code on the simulator -- `Device Simulator Express: [Circuit Playground Express] Deploy to Device`: Copies & Pastes the code.py or main.py file to CIRCUITPY drive if detected a CPX is plugged in -- `Device Simulator Express: [Circuit Playground Express] Open Serial Monitor`: Opens the serial monitor in the integrated output window. -- `Device Simulator Express: [Circuit Playground Express] Close Serial Monitor`: Stops the serial monitor and releases the serial port. -- `Device Simulator Express: [Circuit Playground Express] Change Baud Rate`: Changes the baud rate of the selected serial port. For Adafruit CPX, the default baud rate is 115200. -- `Device Simulator Express: [Circuit Playground Express] Select Serial Port`: Changes the current serial port. +- `Device Simulator Express: [Circuit Playground Express] Deploy to Device`: Copies & Pastes the current file to CIRCUITPY drive if detected a CPX is plugged in +- `Device Simulator Express: Open Serial Monitor`: Opens the serial monitor in the integrated output window. +- `Device Simulator Express: Close Serial Monitor`: Stops the serial monitor and releases the serial port. +- `Device Simulator Express: Change Baud Rate`: Changes the baud rate of the selected serial port. For Adafruit CPX and the BBC micro:bit, the default baud rate is 115200. +- `Device Simulator Express: Select Serial Port`: Changes the current serial port. ### Keybindings @@ -148,6 +159,8 @@ In Device Simulator Express, you can use keyboard to interact with the device: - IntelliSense and syntax highlighting for micro:bit code - Template file generation - Integrated Python Debugging for the Simulator +- Deploy MicroPython code to the physical device +- Serial monitor (available on Windows and Mac only) - Simulation of the micro:bit device, including: - 25 LEDs - Light sensor @@ -240,4 +253,3 @@ A `ThirdPartyNotices.txt` file is provided in the extension's source code listin 1. Make sure that when you type _python_ in a terminal, the command is recognized and you have the correct version. The easiest way to do it is to select the "Add to PATH" option directly when you install Python. Otherwise you can search how to insert it manually. 2. You can choose to see the prompt or not by changing the extension configurations. -3. To be able to run the file on your physical device, it should either be named code.py or main.py. diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 26648250b..f062565a7 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -5829,6 +5829,26 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +uflash +The MIT License (MIT) +Copyright (c) 2015-2018 Nicholas H.Tollervey and others. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------- ------------------------------------------------------------------- diff --git a/docs/how-to-use.md b/docs/how-to-use.md index 09c497710..d94b4987c 100644 --- a/docs/how-to-use.md +++ b/docs/how-to-use.md @@ -9,14 +9,14 @@ Commands are accessible through : - **Open Simulator** : opens the webview of the simulator. -- **New File** : opens an unsaved file with links to help you and a code snippet that you can save as `code.py` / `main.py`. +- **New File** : opens an unsaved file to help you and a code snippet that you can save and play around with. _(**Note :** will open the simulator webview if it's not open yet)_. - **Run Simulator** : runs the code you have opened in the simulator (make sure you've clicked on a valid code file). _(**Note :** will open the simulator webview if it's not open yet)_. -- **Deploy to Device** : saves the code to a Circuit Playground Express. - _(**Note :** the board needs to be correctly formatted to a `CIRCUITPY` drive first. If that's not the case check [Installing CircuitPython](https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython) to correctly format it)_. +- **Deploy to Device** : saves the code to the connected device. + _(**Note :** For the Circuit Playground Express, the board needs to be correctly formatted to a `CIRCUITPY` drive first. If that's not the case check [Installing CircuitPython](https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython) to correctly format it)_. * **Select Serial Port** : selects the serial port of the board you want the serial monitor to interact with. (2) _(**Note :** USB detection must be enabled in the extension settings.)_ @@ -82,7 +82,7 @@ Here are the settings you can change in the Device Simulator Express configurati - The first time you install the extension, you'll need to execute the `run` command at least once in order to access auto-completion. - While running a code file, if you get an error saying it can't find the file, make sure you've clicked on a valid Python code file before running it. - To open the output panel again after closing it go to VS Code menu : `View->Output`. -- If you try to deploy to the device while it's plugged in but you still get an error saying it cannot find the board, make sure your Circuit Playground Express is formatted correctly and that its name matches `CIRCUITPY`. +- For the Circuit Playground Express, if you try to deploy to the device while it's plugged in but you still get an error saying it cannot find the board, make sure your Circuit Playground Express is formatted correctly and that its name matches `CIRCUITPY`. - If you can't get the Simulator communication working while debugging, try to open you `Settings` and check the port used under `'Device Simulator Express: Debugger Server Port'`. You can either change it (usually ports above 5000 could work) or try to free it, then start debugging again. ### Notes diff --git a/gulpfile.js b/gulpfile.js index e2b15d39b..def76e5c3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,6 +39,7 @@ const pythonToMove = [ "./src/microbit/!(test)/**/*", "./src/*.py", "./src/common/*.py", + "./src/dev-requirements.txt", "./src/requirements.txt", "./src/templates/*.*" ]; diff --git a/locales/en/out/constants.i18n.json b/locales/en/out/constants.i18n.json index 333d9bf47..d6c6c548a 100644 --- a/locales/en/out/constants.i18n.json +++ b/locales/en/out/constants.i18n.json @@ -12,10 +12,6 @@ "dialogResponses.readInstall": "Read installation docs", "error.debuggerServerInitFailed": "Warning : The Debugger Server cannot be opened. Please try to free the port {0} if it's already in use or select another one in your Settings 'Device Simulator Express: Debugger Server Port' and start another debug session.\n You can still debug your code but you won't be able to use the Simulator.", "error.debuggingSessionInProgress": "[ERROR] A debugging session is currently in progress, please stop it before running your code. \n", - "error.incorrectFileNameForDevice": "[ERROR] Can\\'t deploy to your Circuit Playground Express device, please rename your file to \"code.py\" or \"main.py\". \n", - "error.incorrectFileNameForDevicePopup": "Seems like you have a different file name than what the CPX requires, please rename it to \"code.py\" or \"main.py\".", - "error.incorrectFileNameForSimulatorPopup": "We want your code to work on your actual board as well. Make sure you name your file \"code.py\" or \"main.py\" to be able to run your code on an actual physical device.", - "error.invalidFileNameDebug": "The file you tried to debug isn\\'t named \"code.py\" or \"main.py\\. Rename your file if you want your code to work on your actual device.", "error.noDevice": "No plugged in boards detected. Please double check if your board is connected and/or properly formatted", "error.noFileToRun": "\n[ERROR] We can't find the .py file to run on simulator. Open up a new .py file, or browse through some examples\n", "error.noFolderCreated": "In order to use the Serial Monitor, you need to open a folder and reload VS Code.", diff --git a/locales/en/package.i18n.json b/locales/en/package.i18n.json index 9afd1a72c..6681f33da 100644 --- a/locales/en/package.i18n.json +++ b/locales/en/package.i18n.json @@ -2,18 +2,19 @@ "deviceSimulatorExpressExtension.commands.common.installDependencies": "Install Extension Dependencies", "deviceSimulatorExpressExtension.commands.common.label": "Device Simulator Express", "deviceSimulatorExpressExtension.commands.common.runSimulator": "Run Simulator", - "deviceSimulatorExpressExtension.commands.cpx.changeBaudRate": "[Circuit Playground Express] Change Baud Rate", - "deviceSimulatorExpressExtension.commands.cpx.closeSerialMonitor": "[Circuit Playground Express] Close Serial Monitor", - "deviceSimulatorExpressExtension.commands.cpx.openSerialMonitor": "[Circuit Playground Express] Open Serial Monitor", + "deviceSimulatorExpressExtension.commands.common.changeBaudRate": "Change Baud Rate", + "deviceSimulatorExpressExtension.commands.common.closeSerialMonitor": "Close Serial Monitor", + "deviceSimulatorExpressExtension.commands.common.openSerialMonitor": "Open Serial Monitor", + "deviceSimulatorExpressExtension.commands.common.selectSerialPort": "Select Serial Port", "deviceSimulatorExpressExtension.commands.cpx.openSimulator": "[Circuit Playground Express] Open Simulator", "deviceSimulatorExpressExtension.commands.cpx.newFile": "[Circuit Playground Express] New File", "deviceSimulatorExpressExtension.commands.cpx.deployToDevice": "[Circuit Playground Express] Deploy to Device", - "deviceSimulatorExpressExtension.commands.cpx.selectSerialPort": "[Circuit Playground Express] Select Serial Port", + "deviceSimulatorExpressExtension.commands.microbit.deployToDevice": "[micro:bit] Deploy to Device", "deviceSimulatorExpressExtension.commands.microbit.openSimulator": "[micro:bit] Open Simulator", "deviceSimulatorExpressExtension.commands.microbit.newFile": "[micro:bit] New File", "deviceSimulatorExpressExtension.configuration.title": "Device Simulator Express configuration", "deviceSimulatorExpressExtension.configuration.properties.configEnvOnChange": "When you change the Python interpreter, the Device Simulator Express will automatically configure itself for the required dependencies.", "deviceSimulatorExpressExtension.configuration.properties.debuggerPort": "The port the Server will listen on for communication with the debugger.", - "deviceSimulatorExpressExtension.configuration.properties.dependencyChecker": "Whether or not to ask if we can download dependencies. If unchecked, the extension will default to never download dependencies, except when automatically creating a virtual environment in the extension files." + "deviceSimulatorExpressExtension.configuration.properties.dependencyChecker": "Whether or not to ask if we can download dependencies. If unchecked, the extension will default to never download dependencies, except when automatically creating a virtual environment in the extension files.", "deviceSimulatorExpressExtension.configuration.properties.previewMode": "Enable this to test out and play with the new micro:bit simulator!" } diff --git a/package.json b/package.json index 0c716c8f6..4c18fe24a 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,13 @@ ], "activationEvents": [ "onCommand:deviceSimulatorExpress.common.installDependencies", + "onCommand:deviceSimulatorExpress.common.openSerialMonitor", "onCommand:deviceSimulatorExpress.common.runSimulator", + "onCommand:deviceSimulatorExpress.common.selectSerialPort", "onCommand:deviceSimulatorExpress.cpx.deployToDevice", "onCommand:deviceSimulatorExpress.cpx.newFile", - "onCommand:deviceSimulatorExpress.cpx.openSerialMonitor", "onCommand:deviceSimulatorExpress.cpx.openSimulator", - "onCommand:deviceSimulatorExpress.cpx.selectSerialPort", + "onCommand:deviceSimulatorExpress.microbit.deployToDevice", "onCommand:deviceSimulatorExpress.microbit.newFile", "onCommand:deviceSimulatorExpress.microbit.openSimulator", "onDebug" @@ -50,18 +51,28 @@ "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, { - "command": "deviceSimulatorExpress.common.runSimulator", - "title": "%deviceSimulatorExpressExtension.commands.common.runSimulator%", + "command": "deviceSimulatorExpress.common.changeBaudRate", + "title": "%deviceSimulatorExpressExtension.commands.common.changeBaudRate%", + "category": "%deviceSimulatorExpressExtension.commands.common.label%" + }, + { + "command": "deviceSimulatorExpress.common.closeSerialMonitor", + "title": "%deviceSimulatorExpressExtension.commands.common.closeSerialMonitor%", "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, { - "command": "deviceSimulatorExpress.cpx.changeBaudRate", - "title": "%deviceSimulatorExpressExtension.commands.cpx.changeBaudRate%", + "command": "deviceSimulatorExpress.common.openSerialMonitor", + "title": "%deviceSimulatorExpressExtension.commands.common.openSerialMonitor%", "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, { - "command": "deviceSimulatorExpress.cpx.closeSerialMonitor", - "title": "%deviceSimulatorExpressExtension.commands.cpx.closeSerialMonitor%", + "command": "deviceSimulatorExpress.common.selectSerialPort", + "title": "%deviceSimulatorExpressExtension.commands.common.selectSerialPort%", + "category": "%deviceSimulatorExpressExtension.commands.common.label%" + }, + { + "command": "deviceSimulatorExpress.common.runSimulator", + "title": "%deviceSimulatorExpressExtension.commands.common.runSimulator%", "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, { @@ -74,19 +85,14 @@ "title": "%deviceSimulatorExpressExtension.commands.cpx.newFile%", "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, - { - "command": "deviceSimulatorExpress.cpx.openSerialMonitor", - "title": "%deviceSimulatorExpressExtension.commands.cpx.openSerialMonitor%", - "category": "%deviceSimulatorExpressExtension.commands.common.label%" - }, { "command": "deviceSimulatorExpress.cpx.openSimulator", "title": "%deviceSimulatorExpressExtension.commands.cpx.openSimulator%", "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, { - "command": "deviceSimulatorExpress.cpx.selectSerialPort", - "title": "%deviceSimulatorExpressExtension.commands.cpx.selectSerialPort%", + "command": "deviceSimulatorExpress.microbit.deployToDevice", + "title": "%deviceSimulatorExpressExtension.commands.microbit.deployToDevice%", "category": "%deviceSimulatorExpressExtension.commands.common.label%" }, { @@ -102,6 +108,12 @@ ], "menus": { "commandPalette": [ + { + "command": "deviceSimulatorExpress.microbit.deployToDevice", + "title": "%deviceSimulatorExpressExtension.commands.microbit.deployToDevice%", + "category": "%deviceSimulatorExpressExtension.commands.common.label%", + "when": "config.deviceSimulatorExpress.previewMode" + }, { "command": "deviceSimulatorExpress.microbit.openSimulator", "title": "%deviceSimulatorExpressExtension.commands.microbit.openSimulator%", diff --git a/package.nls.json b/package.nls.json index 1092a8d4a..2f574c7eb 100644 --- a/package.nls.json +++ b/package.nls.json @@ -2,13 +2,14 @@ "deviceSimulatorExpressExtension.commands.common.installDependencies": "Install Extension Dependencies", "deviceSimulatorExpressExtension.commands.common.label": "Device Simulator Express", "deviceSimulatorExpressExtension.commands.common.runSimulator": "Run Simulator", - "deviceSimulatorExpressExtension.commands.cpx.changeBaudRate": "[Circuit Playground Express] Change Baud Rate", - "deviceSimulatorExpressExtension.commands.cpx.closeSerialMonitor": "[Circuit Playground Express] Close Serial Monitor", - "deviceSimulatorExpressExtension.commands.cpx.openSerialMonitor": "[Circuit Playground Express] Open Serial Monitor", + "deviceSimulatorExpressExtension.commands.common.changeBaudRate": "Change Baud Rate", + "deviceSimulatorExpressExtension.commands.common.closeSerialMonitor": "Close Serial Monitor", + "deviceSimulatorExpressExtension.commands.common.openSerialMonitor": "Open Serial Monitor", + "deviceSimulatorExpressExtension.commands.common.selectSerialPort": "Select Serial Port", "deviceSimulatorExpressExtension.commands.cpx.openSimulator": "[Circuit Playground Express] Open Simulator", "deviceSimulatorExpressExtension.commands.cpx.newFile": "[Circuit Playground Express] New File", "deviceSimulatorExpressExtension.commands.cpx.deployToDevice": "[Circuit Playground Express] Deploy to Device", - "deviceSimulatorExpressExtension.commands.cpx.selectSerialPort": "[Circuit Playground Express] Select Serial Port", + "deviceSimulatorExpressExtension.commands.microbit.deployToDevice": "[micro:bit] Deploy to Device", "deviceSimulatorExpressExtension.commands.microbit.openSimulator": "[micro:bit] Open Simulator", "deviceSimulatorExpressExtension.commands.microbit.newFile": "[micro:bit] New File", "deviceSimulatorExpressExtension.configuration.title": "Device Simulator Express configuration", diff --git a/src/adafruit_circuitplayground/test/test_utils.py b/src/adafruit_circuitplayground/test/test_utils.py index a3c38e1b1..21a8c6296 100644 --- a/src/adafruit_circuitplayground/test/test_utils.py +++ b/src/adafruit_circuitplayground/test/test_utils.py @@ -13,16 +13,20 @@ def test_remove_leading_slashes(self): assert expected == utils.remove_leading_slashes(original) def test_escape_notOSX(self): + _utils_sys = utils.sys if sys.platform.startswith(CONSTANTS.MAC_OS): utils.sys = mock.MagicMock() utils.sys.configure_mock(platform="win32") original = "a b" assert original == utils.escape_if_OSX(original) + utils.sys = _utils_sys def test_escape_isOSX(self): + _utils_sys = utils.sys if not sys.platform.startswith(CONSTANTS.MAC_OS): utils.sys = mock.MagicMock() utils.sys.configure_mock(platform="darwin") original = "a b" expected = "a%20b" assert expected == utils.escape_if_OSX(original) + utils.sys = _utils_sys diff --git a/src/common/utils.py b/src/common/utils.py index a468044f7..6a0e6953d 100644 --- a/src/common/utils.py +++ b/src/common/utils.py @@ -44,6 +44,6 @@ def remove_leading_slashes(string): def escape_if_OSX(file_name): - if sys.platform.startswith(CONSTANTS.MAC_OS): + if sys.platform == CONSTANTS.MAC_OS: file_name = file_name.replace(" ", "%20") return file_name diff --git a/src/constants.ts b/src/constants.ts index 7c537c122..5d30b3602 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -65,18 +65,11 @@ export const CONSTANTS = { `[ERROR] Failed to open serial port ${port} due to error: ${error}. \n` ); }, - INCORRECT_FILE_NAME_FOR_DEVICE: localize( - "error.incorrectFileNameForDevice", - '[ERROR] Can\'t deploy to your Circuit Playground Express device, please rename your file to "code.py" or "main.py". \n' - ), - INCORRECT_FILE_NAME_FOR_DEVICE_POPUP: localize( - "error.incorrectFileNameForDevicePopup", - 'Seems like you have a different file name than what CPX requires, please rename it to "code.py" or "main.py".' - ), INSTALLATION_ERROR: localize( "error.installationError", "Installation Error" ), + INVALID_FILE_EXTENSION_DEBUG: localize( "error.invalidFileExtensionDebug", "The file you tried to run isn't a Python file." @@ -85,6 +78,10 @@ export const CONSTANTS = { "error.invalidPythonPath", 'We found that your selected Python interpreter version is too low to run the extension. Please upgrade to version 3.7+ or select a different interpreter (CTRL+SHIFT+P and type "python.selectInterpreter") and restart the application.' ), + LOW_PYTHON_VERSION_FOR_MICROBIT_DEPLOYMENT: localize( + "error.lowPythonVersionForMicrobitDeployment", + "To deploy your code to the micro:bit, you must be using Python 3.3+" + ), NO_DEVICE: localize( "error.noDevice", "No plugged in boards detected. Please double check if your board is connected and/or properly formatted" @@ -155,11 +152,11 @@ export const CONSTANTS = { ), DEPLOY_SUCCESS: localize( "info.deploySuccess", - "\n[INFO] Code successfully copied! Your Circuit Playground Express should be loading and ready to go shortly.\n" + "\n[INFO] Code successfully copied! Your device should be loading and ready to go shortly.\n" ), EXTENSION_ACTIVATED: localize( "info.extensionActivated", - "Congratulations, your extension Adafruit_Simulator is now active!" + "Congratulations, your extension Device Simulator Express is now active!" ), FILE_SELECTED: (filePath: string) => { return localize( @@ -171,10 +168,6 @@ export const CONSTANTS = { "info.firstTimeWebview", 'To reopen the simulator select the command "Open Simulator" from command palette.' ), - INCORRECT_FILE_NAME_FOR_SIMULATOR_POPUP: localize( - "info.incorrectFileNameForSimulatorPopup", - 'We want your code to work on your actual board as well. Make sure you name your file "code.py" or "main.py" to be able to run your code on an actual physical device' - ), INSTALLING_PYTHON_VENV: localize( "info.installingPythonVenv", "A virtual environment is currently being created. The required Python packages will be installed. You will be prompted a message telling you when the installation is done." @@ -187,10 +180,6 @@ export const CONSTANTS = { "info.installPythonVenv", "Do you want us to try and install this extension's dependencies via virtual environment for you?" ), - INVALID_FILE_NAME_DEBUG: localize( - "info.invalidFileNameDebug", - 'The file you tried to debug isn\'t named "code.py" or "main.py". Rename your file if you want your code to work on your actual device.' - ), NEW_FILE: localize( "info.newFile", "New to Python or the Circuit Playground Express? We are here to help!" @@ -327,15 +316,16 @@ export enum TelemetryEventName { COMMAND_RUN_SIMULATOR_BUTTON = "COMMAND.RUN.SIMULATOR_BUTTON", COMMAND_RUN_PALETTE = "COMMAND.RUN.PALETTE", COMMAND_INSTALL_EXTENSION_DEPENDENCIES = "COMMAND.INSTALL.EXTENSION.DEPENDENCIES", + COMMAND_SERIAL_MONITOR_CHOOSE_PORT = "COMMAND.SERIAL_MONITOR.CHOOSE_PORT", + COMMAND_SERIAL_MONITOR_OPEN = "COMMAND.SERIAL_MONITOR.OPEN", + COMMAND_SERIAL_MONITOR_BAUD_RATE = "COMMAND.SERIAL_MONITOR.BAUD_RATE", + COMMAND_SERIAL_MONITOR_CLOSE = "COMMAND.SERIAL_MONITOR.CLOSE", CPX_COMMAND_DEPLOY_DEVICE = "CPX.COMMAND.DEPLOY.DEVICE", CPX_COMMAND_NEW_FILE = "CPX.COMMAND.NEW.FILE.CPX", CPX_COMMAND_OPEN_SIMULATOR = "CPX.COMMAND.OPEN.SIMULATOR", - CPX_COMMAND_SERIAL_MONITOR_CHOOSE_PORT = "CPX.COMMAND.SERIAL_MONITOR.CHOOSE_PORT", - CPX_COMMAND_SERIAL_MONITOR_OPEN = "CPX.COMMAND.SERIAL_MONITOR.OPEN", - CPX_COMMAND_SERIAL_MONITOR_BAUD_RATE = "CPX.COMMAND.SERIAL_MONITOR.BAUD_RATE", - CPX_COMMAND_SERIAL_MONITOR_CLOSE = "CPX.COMMAND.SERIAL_MONITOR.CLOSE", + MICROBIT_COMMAND_DEPLOY_DEVICE = "MICROBIT.COMMAND.DEPLOY.DEVICE", MICROBIT_COMMAND_NEW_FILE = "MICROBIT.COMMAND.NEW.FILE", MICROBIT_COMMAND_OPEN_SIMULATOR = "MICROBIT.COMMAND.OPEN.SIMULATOR", @@ -373,14 +363,32 @@ export enum TelemetryEventName { CPX_SUCCESS_COMMAND_DEPLOY_DEVICE = "CPX.SUCCESS.COMMAND.DEPLOY.DEVICE", MICROBIT_ERROR_COMMAND_NEW_FILE = "MICROBIT.ERROR.COMMAND.NEW.FILE", + MICROBIT_ERROR_DEPLOY_WITHOUT_DEVICE = "MICROBIT.ERROR.DEPLOY.WITHOUT.DEVICE", + MICROBIT_ERROR_PYTHON_DEVICE_PROCESS = "MICROBIT.ERROR.PYTHON.DEVICE.PROCESS", + MICROBIT_SUCCESS_COMMAND_DEPLOY_DEVICE = "MICROBIT.SUCCESS.COMMAND.DEPLOY.DEVICE", // Performance CPX_PERFORMANCE_DEPLOY_DEVICE = "CPX.PERFORMANCE.DEPLOY.DEVICE", CPX_PERFORMANCE_NEW_FILE = "CPX.PERFORMANCE.NEW.FILE", CPX_PERFORMANCE_OPEN_SIMULATOR = "CPX.PERFORMANCE.OPEN.SIMULATOR", + MICROBIT_PERFORMANCE_DEPLOY_DEVICE = "MICROBIT.PERFORMANCE.DEPLOY.DEVICE", MICROBIT_PERFORMANCE_NEW_FILE = "MICROBIT.PERFORMANCE.NEW.FILE", MICROBIT_PERFORMANCE_OPEN_SIMULATOR = "MICROBIT.PERFORMANCE.OPEN.SIMULATOR", + + // Venv options + SETUP_VENV_CREATION_ERR = "SETUP.VENV.CREATION.ERR", + SETUP_NO_PIP = "SETUP.NO.PIP", + SETUP_DEP_INSTALL_FAIL = "SETUP.DEP.INSTALL.FAIL", + SETUP_AUTO_RESOLVE_PYTHON_PATH = "SETUP.AUTO.RESOLVE.PYTHON.PATH", + SETUP_NO_PYTHON_PATH = "SETUP.NO.PYTHON.PATH", + SETUP_DOWNLOAD_PYTHON = "SETUP.DOWNLOAD.PYTHON", + SETUP_INVALID_PYTHON_INTERPRETER_PATH = "SETUP.INVALID.PYTHON.INTERPRETER.PATH", + SETUP_INVALID_PYTHON_VER = "SETUP.INVALID.PYTHON.VER", + SETUP_INSTALL_VENV = "SETUP.INSTALL.VENV", + SETUP_ORIGINAL_INTERPRETER_DEP_INSTALL = "SETUP.ORIGINAL.INTERPRETER.DEP.INSTALL", + SETUP_HAS_VENV = "SETUP.HAS.VENV", + SETUP_NO_DEPS_INSTALLED = "SETUP.NO.DEPS.INSTALLED", } export const DEFAULT_DEVICE = CONSTANTS.DEVICE_NAME.CPX; @@ -450,11 +458,6 @@ export namespace DialogResponses { export const CPX_CONFIG_FILE = path.join(".vscode", "cpx.json"); -export const USER_CODE_NAMES = { - CODE_PY: "code.py", - MAIN_PY: "main.py", -}; - export const STATUS_BAR_PRIORITY = { PORT: 20, OPEN_PORT: 30, @@ -477,5 +480,8 @@ export const HELPER_FILES = { export const GLOBAL_ENV_VARS = { PYTHON: "python", }; +export const LANGUAGE_VARS = { + PYTHON: { ID: "python", FILE_ENDS: ".py" }, +}; export default CONSTANTS; diff --git a/src/debug_user_code.py b/src/debug_user_code.py index c4b3dd2c5..c7110640f 100644 --- a/src/debug_user_code.py +++ b/src/debug_user_code.py @@ -51,7 +51,7 @@ mb._MicrobitModel__set_debug_mode(True) # Execute the user's code file -with open(abs_path_to_code_file) as user_code_file: +with open(abs_path_to_code_file, encoding="utf8") as user_code_file: user_code = user_code_file.read() try: codeObj = compile(user_code, abs_path_to_code_file, CONSTANTS.EXEC_COMMAND) diff --git a/src/dev-requirements.txt b/src/dev-requirements.txt new file mode 100644 index 000000000..26f334b51 --- /dev/null +++ b/src/dev-requirements.txt @@ -0,0 +1,3 @@ +-r ./requirements.txt +black==19.10b0 +pytest==5.0.1 \ No newline at end of file diff --git a/src/device.py b/src/device.py index 60b081b50..cc6e035ff 100644 --- a/src/device.py +++ b/src/device.py @@ -5,7 +5,9 @@ import string import os import sys +import shutil import json +import uflash import python_constants as CONSTANTS if sys.platform == "win32": @@ -14,11 +16,13 @@ class Device: - def __init__(self): + def __init__(self, name, file_path): + self.name = name + self.file_path = file_path self.connected = False self.error_message = None - def find_device_directory(self): + def find_cpx_directory(self): """ Check if the Circuit Playground Express is available/plugged in """ @@ -61,22 +65,57 @@ def find_device_directory(self): self.error_message = None return found_directory + def deployToCPX(self): + device_directory = self.find_cpx_directory() + if self.error_message: + print( + "{}:\t{}".format(self.error_message[0], self.error_message[1]), + file=sys.stderr, + flush=True, + ) + if self.connected: + original_file_name = self.file_path.rsplit(os.sep, 1)[-1] + if original_file_name == "code.py" or original_file_name == "main.py": + dest_path = os.path.join(device_directory, original_file_name) + else: + dest_path = os.path.join(device_directory, "code.py") + shutil.copyfile(self.file_path, dest_path) + message = {"type": "complete"} + else: + message = {"type": "no-device"} + return message -if __name__ == "__main__": - import shutil + def deployToMicrobit(self): + # Temporarily redirecting stdout because there are some print statements in uflash library + fake_stdout = open(os.devnull, "w") + _stdout = sys.stdout + sys.stdout = fake_stdout + + try: + uflash.flash(path_to_python=self.file_path) + message = {"type": "complete"} + except RuntimeError: + message = {"type": "low-python-version"} + except IOError: + self.error_message = CONSTANTS.NO_MICROBIT_DETECTED_ERROR_TITLE + print( + self.error_message, file=sys.stderr, flush=True, + ) + message = {"type": "no-device"} - cpx = Device() - device_directory = cpx.find_device_directory() - if cpx.error_message: - print( - "{}:\t{}".format(cpx.error_message[0], cpx.error_message[1]), - file=sys.stderr, - flush=True, - ) - if cpx.connected: - dest_path = os.path.join(device_directory, sys.argv[1].rsplit(os.sep, 1)[-1]) - shutil.copyfile(sys.argv[1], dest_path) - message = {"type": "complete"} - else: - message = {"type": "no-device"} + sys.stdout = _stdout + return message + + def deploy(self): + if self.name == CONSTANTS.MICROBIT: + return self.deployToMicrobit() + elif self.name == CONSTANTS.CPX: + return self.deployToCPX() + else: + return {"type": "no-device"} + + +if __name__ == "__main__": + device = Device(sys.argv[1], sys.argv[2]) + message = device.deploy() print(json.dumps(message), flush=True) diff --git a/src/extension.ts b/src/extension.ts index c14532a0a..59d9d87e8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import * as cp from "child_process"; import * as fs from "fs"; +import { registerDefaultFontFaces } from "office-ui-fabric-react"; import * as open from "open"; import * as os from "os"; import * as path from "path"; @@ -17,6 +18,7 @@ import { HELPER_FILES, SERVER_INFO, TelemetryEventName, + LANGUAGE_VARS, } from "./constants"; import { CPXWorkspace } from "./cpxWorkspace"; import { DebugAdapterFactory } from "./debugger/debugAdapterFactory"; @@ -24,62 +26,46 @@ import { DebuggerCommunicationServer } from "./debuggerCommunicationServer"; import * as utils from "./extension_utils/utils"; import { SerialMonitor } from "./serialMonitor"; import { DebuggerCommunicationService } from "./service/debuggerCommunicationService"; +import { DeviceSelectionService } from "./service/deviceSelectionService"; +import { FileSelectionService } from "./service/fileSelectionService"; import { MessagingService } from "./service/messagingService"; +import { PopupService } from "./service/PopupService"; +import { SetupService } from "./service/SetupService"; import { SimulatorDebugConfigurationProvider } from "./simulatorDebugConfigurationProvider"; +import getPackageInfo from "./telemetry/getPackageInfo"; import TelemetryAI from "./telemetry/telemetryAI"; import { UsbDetector } from "./usbDetector"; import { VSCODE_MESSAGES_TO_WEBVIEW, WEBVIEW_MESSAGES } from "./view/constants"; -import { PopupService } from "./service/PopupService"; -import getPackageInfo from "./telemetry/getPackageInfo"; -import { registerDefaultFontFaces } from "office-ui-fabric-react"; -let currentFileAbsPath: string = ""; -let currentTextDocument: vscode.TextDocument; let telemetryAI: TelemetryAI; let pythonExecutablePath: string = GLOBAL_ENV_VARS.PYTHON; let configFileCreated: boolean = false; let inDebugMode: boolean = false; // Notification booleans let firstTimeClosed: boolean = true; -let shouldShowInvalidFileNamePopup: boolean = true; let shouldShowRunCodePopup: boolean = true; -const messagingService = new MessagingService(); -const debuggerCommunicationService = new DebuggerCommunicationService(); -let currentActiveDevice: string = DEFAULT_DEVICE; +let setupService: SetupService; +const deviceSelectionService = new DeviceSelectionService(); +const messagingService = new MessagingService(deviceSelectionService); +const debuggerCommunicationService = new DebuggerCommunicationService(); +const fileSelectionService = new FileSelectionService(messagingService); export let outChannel: vscode.OutputChannel | undefined; function loadScript(context: vscode.ExtensionContext, scriptPath: string) { - return ``; } -const setPathAndSendMessage = ( - currentPanel: vscode.WebviewPanel, - newFilePath: string -) => { - currentFileAbsPath = newFilePath; - if (currentPanel) { - currentPanel.webview.postMessage({ - command: "current-file", - active_device: currentActiveDevice, - - state: { - running_file: newFilePath, - }, - }); - } -}; - const sendCurrentDeviceMessage = (currentPanel: vscode.WebviewPanel) => { if (currentPanel) { currentPanel.webview.postMessage({ command: VSCODE_MESSAGES_TO_WEBVIEW.SET_DEVICE, - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), }); } }; @@ -88,6 +74,7 @@ export async function activate(context: vscode.ExtensionContext) { console.info(CONSTANTS.INFO.EXTENSION_ACTIVATED); telemetryAI = new TelemetryAI(context); + setupService = new SetupService(telemetryAI); let currentPanel: vscode.WebviewPanel | undefined; let childProcess: cp.ChildProcess | undefined; let messageListener: vscode.Disposable; @@ -100,7 +87,7 @@ export async function activate(context: vscode.ExtensionContext) { // doesn't trigger lint errors updatePylintArgs(context); - pythonExecutablePath = await utils.setupEnv(context); + pythonExecutablePath = await setupService.setupEnv(context); try { utils.generateCPXConfig(); @@ -121,7 +108,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidSaveTextDocument( async (document: vscode.TextDocument) => { - await updateCurrentFileIfPython(document, currentPanel); + await fileSelectionService.updateCurrentFileFromTextFile(document); } ); @@ -138,7 +125,7 @@ export async function activate(context: vscode.ExtensionContext) { } const openWebview = () => { - if (currentPanel) { + if (currentPanel && currentPanel.webview) { messagingService.setWebview(currentPanel.webview); currentPanel.reveal(vscode.ViewColumn.Beside); } else { @@ -186,7 +173,7 @@ export async function activate(context: vscode.ExtensionContext) { messageListener = currentPanel.webview.onDidReceiveMessage( message => { const messageJson = JSON.stringify({ - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), state: message.text, }); switch (message.command) { @@ -210,18 +197,10 @@ export async function activate(context: vscode.ExtensionContext) { case WEBVIEW_MESSAGES.TOGGLE_PLAY_STOP: console.log(`Play button ${messageJson} \n`); if (message.text.state as boolean) { - setPathAndSendMessage( - currentPanel, + fileSelectionService.setPathAndSendMessage( message.text.selected_file ); - if (currentFileAbsPath) { - const foundDocument = utils.getActiveEditorFromPath( - currentFileAbsPath - ); - if (foundDocument !== undefined) { - currentTextDocument = foundDocument; - } - } + fileSelectionService.findCurrentTextDocument(); telemetryAI.trackFeatureUsage( TelemetryEventName.COMMAND_RUN_SIMULATOR_BUTTON ); @@ -262,7 +241,9 @@ export async function activate(context: vscode.ExtensionContext) { handleSensorTelemetry(message.text); break; case WEBVIEW_MESSAGES.SWITCH_DEVICE: - switchDevice(message.text.active_device); + deviceSelectionService.setCurrentActiveDevice( + message.text.active_device + ); killProcessIfRunning(); break; default: @@ -309,12 +290,16 @@ export async function activate(context: vscode.ExtensionContext) { }; const openCPXWebview = () => { - switchDevice(CONSTANTS.DEVICE_NAME.CPX); + deviceSelectionService.setCurrentActiveDevice( + CONSTANTS.DEVICE_NAME.CPX + ); openWebview(); }; const openMicrobitWebview = () => { - switchDevice(CONSTANTS.DEVICE_NAME.MICROBIT); + deviceSelectionService.setCurrentActiveDevice( + CONSTANTS.DEVICE_NAME.MICROBIT + ); openWebview(); }; @@ -346,12 +331,16 @@ export async function activate(context: vscode.ExtensionContext) { ); const openCPXTemplateFile = () => { - switchDevice(CONSTANTS.DEVICE_NAME.CPX); + deviceSelectionService.setCurrentActiveDevice( + CONSTANTS.DEVICE_NAME.CPX + ); openTemplateFile(CONSTANTS.TEMPLATE.CPX); }; const openMicrobitTemplateFile = () => { - switchDevice(CONSTANTS.DEVICE_NAME.MICROBIT); + deviceSelectionService.setCurrentActiveDevice( + CONSTANTS.DEVICE_NAME.MICROBIT + ); openTemplateFile(CONSTANTS.TEMPLATE.MICROBIT); }; @@ -402,7 +391,10 @@ export async function activate(context: vscode.ExtensionContext) { // tslint:disable-next-line: ban-comma-operator vscode.workspace - .openTextDocument({ content: file, language: "python" }) + .openTextDocument({ + content: file, + language: LANGUAGE_VARS.PYTHON.ID, + }) .then((template: vscode.TextDocument) => { vscode.window.showTextDocument(template, 1, false).then(() => { openWebview(); @@ -444,7 +436,7 @@ export async function activate(context: vscode.ExtensionContext) { const installDependencies: vscode.Disposable = vscode.commands.registerCommand( "deviceSimulatorExpress.common.installDependencies", async () => { - pythonExecutablePath = await utils.setupEnv(context, true); + pythonExecutablePath = await setupService.setupEnv(context, true); telemetryAI.trackFeatureUsage( TelemetryEventName.COMMAND_INSTALL_EXTENSION_DEPENDENCIES ); @@ -454,10 +446,9 @@ export async function activate(context: vscode.ExtensionContext) { const killProcessIfRunning = () => { if (childProcess !== undefined) { if (currentPanel) { - console.info("Sending clearing state command"); currentPanel.webview.postMessage({ command: "reset-state", - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), }); } // TODO: We need to check the process was correctly killed @@ -511,12 +502,11 @@ export async function activate(context: vscode.ExtensionContext) { killProcessIfRunning(); - await updateCurrentFileIfPython( - vscode.window.activeTextEditor!.document, - currentPanel + await fileSelectionService.updateCurrentFileFromEditor( + vscode.window.activeTextEditor ); - if (currentFileAbsPath === "") { + if (fileSelectionService.getCurrentFileAbsPath() === "") { utils.logToOutputChannel( outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, @@ -528,9 +518,13 @@ export async function activate(context: vscode.ExtensionContext) { ); } else { // Save on run - await currentTextDocument.save(); + await fileSelectionService.getCurrentTextDocument().save(); - if (!currentTextDocument.fileName.endsWith(".py")) { + if ( + !fileSelectionService + .getCurrentTextDocument() + .fileName.endsWith(LANGUAGE_VARS.PYTHON.FILE_ENDS) + ) { utils.logToOutputChannel( outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, @@ -540,34 +534,15 @@ export async function activate(context: vscode.ExtensionContext) { } utils.logToOutputChannel( outChannel, - CONSTANTS.INFO.FILE_SELECTED(currentFileAbsPath) + CONSTANTS.INFO.FILE_SELECTED( + fileSelectionService.getCurrentFileAbsPath() + ) ); - if ( - !utils.validCodeFileName(currentFileAbsPath) && - shouldShowInvalidFileNamePopup - ) { - // to the popup - vscode.window - .showInformationMessage( - CONSTANTS.INFO.INCORRECT_FILE_NAME_FOR_SIMULATOR_POPUP, - DialogResponses.DONT_SHOW, - DialogResponses.MESSAGE_UNDERSTOOD - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.DONT_SHOW) { - shouldShowInvalidFileNamePopup = false; - telemetryAI.trackFeatureUsage( - TelemetryEventName.CPX_CLICK_DIALOG_DONT_SHOW - ); - } - }); - } - // Activate the run webview button currentPanel.webview.postMessage({ command: "activate-play", - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), }); childProcess = cp.spawn(pythonExecutablePath, [ @@ -576,7 +551,7 @@ export async function activate(context: vscode.ExtensionContext) { CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, HELPER_FILES.PROCESS_USER_CODE_PY ), - currentFileAbsPath, + fileSelectionService.getCurrentFileAbsPath(), JSON.stringify({ enable_telemetry: utils.getTelemetryState() }), ]); @@ -610,10 +585,10 @@ export async function activate(context: vscode.ExtensionContext) { ); if ( messageData.device_name === - currentActiveDevice + deviceSelectionService.getCurrentActiveDevice() ) { currentPanel.webview.postMessage({ - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), command: "set-state", state: messageData, }); @@ -660,9 +635,8 @@ export async function activate(context: vscode.ExtensionContext) { true ); if (currentPanel) { - console.log("Sending clearing state command"); currentPanel.webview.postMessage({ - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), command: "reset-state", }); } @@ -686,21 +660,18 @@ export async function activate(context: vscode.ExtensionContext) { } ); - const cpxDeployCodeToDevice = async () => { - console.info("Sending code to device"); - + const deployCode = async (device: string) => { utils.logToOutputChannel( outChannel, CONSTANTS.INFO.DEPLOY_DEVICE, true ); - await updateCurrentFileIfPython( - vscode.window.activeTextEditor!.document, - currentPanel + await fileSelectionService.updateCurrentFileFromEditor( + vscode.window.activeTextEditor ); - if (currentFileAbsPath === "") { + if (fileSelectionService.getCurrentFileAbsPath() === "") { utils.logToOutputChannel( outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, @@ -710,23 +681,12 @@ export async function activate(context: vscode.ExtensionContext) { CONSTANTS.ERROR.NO_FILE_TO_RUN, DialogResponses.MESSAGE_UNDERSTOOD ); - } else if (!utils.validCodeFileName(currentFileAbsPath)) { - // Save on run - await currentTextDocument.save(); - // Output panel - utils.logToOutputChannel( - outChannel, - CONSTANTS.ERROR.INCORRECT_FILE_NAME_FOR_DEVICE, - true - ); - // Popup - vscode.window.showErrorMessage( - CONSTANTS.ERROR.INCORRECT_FILE_NAME_FOR_DEVICE_POPUP - ); } else { utils.logToOutputChannel( outChannel, - CONSTANTS.INFO.FILE_SELECTED(currentFileAbsPath) + CONSTANTS.INFO.FILE_SELECTED( + fileSelectionService.getCurrentFileAbsPath() + ) ); const deviceProcess = cp.spawn(pythonExecutablePath, [ @@ -735,7 +695,8 @@ export async function activate(context: vscode.ExtensionContext) { CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, HELPER_FILES.DEVICE_PY ), - currentFileAbsPath, + device, + fileSelectionService.getCurrentFileAbsPath(), ]); let dataFromTheProcess = ""; @@ -747,58 +708,14 @@ export async function activate(context: vscode.ExtensionContext) { let messageToWebview; try { messageToWebview = JSON.parse(dataFromTheProcess); - // Check the JSON is a state - switch (messageToWebview.type) { - case "complete": - telemetryAI.trackFeatureUsage( - TelemetryEventName.CPX_SUCCESS_COMMAND_DEPLOY_DEVICE - ); - utils.logToOutputChannel( - outChannel, - CONSTANTS.INFO.DEPLOY_SUCCESS - ); - break; - - case "no-device": - telemetryAI.trackFeatureUsage( - TelemetryEventName.CPX_ERROR_DEPLOY_WITHOUT_DEVICE - ); - vscode.window - .showErrorMessage( - CONSTANTS.ERROR.NO_DEVICE, - DialogResponses.HELP - ) - .then( - ( - selection: - | vscode.MessageItem - | undefined - ) => { - if ( - selection === DialogResponses.HELP - ) { - const okAction = () => { - open(CONSTANTS.LINKS.HELP); - telemetryAI.trackFeatureUsage( - TelemetryEventName.CPX_CLICK_DIALOG_HELP_DEPLOY_TO_DEVICE - ); - }; - utils.showPrivacyModal( - okAction, - CONSTANTS.INFO - .THIRD_PARTY_WEBSITE_ADAFRUIT - ); - } - } - ); - break; - - default: - console.log( - `Non-state JSON output from the process : ${messageToWebview}` - ); - break; + if (messageToWebview.type === "complete") { + utils.logToOutputChannel( + outChannel, + CONSTANTS.INFO.DEPLOY_SUCCESS, + true + ); } + handleDeployToDeviceTelemetry(messageToWebview, device); } catch (err) { console.log( `Non-JSON output from the process : ${dataFromTheProcess}` @@ -808,10 +725,7 @@ export async function activate(context: vscode.ExtensionContext) { // Std error output deviceProcess.stderr.on("data", data => { - telemetryAI.trackFeatureUsage( - TelemetryEventName.CPX_ERROR_PYTHON_DEVICE_PROCESS, - { error: `${data}` } - ); + handleDeployToDeviceErrorTelemetry(data, device); console.error( `Error from the Python device process through stderr: ${data}` ); @@ -829,6 +743,86 @@ export async function activate(context: vscode.ExtensionContext) { } }; + const handleDeployToDeviceErrorTelemetry = ( + data: string, + device: string + ) => { + let telemetryErrorName: string; + if (device === CONSTANTS.DEVICE_NAME.CPX) { + telemetryErrorName = + TelemetryEventName.CPX_ERROR_PYTHON_DEVICE_PROCESS; + } else if (device === CONSTANTS.DEVICE_NAME.MICROBIT) { + telemetryErrorName = + TelemetryEventName.MICROBIT_ERROR_PYTHON_DEVICE_PROCESS; + } + telemetryAI.trackFeatureUsage(telemetryErrorName, { error: `${data}` }); + }; + + const handleDeployToDeviceTelemetry = (message: any, device: string) => { + let successCommandDeployDevice: string; + let errorCommandDeployWithoutDevice: string; + if (device === CONSTANTS.DEVICE_NAME.CPX) { + successCommandDeployDevice = + TelemetryEventName.CPX_SUCCESS_COMMAND_DEPLOY_DEVICE; + errorCommandDeployWithoutDevice = + TelemetryEventName.CPX_ERROR_DEPLOY_WITHOUT_DEVICE; + } else if (device === CONSTANTS.DEVICE_NAME.MICROBIT) { + successCommandDeployDevice = + TelemetryEventName.MICROBIT_SUCCESS_COMMAND_DEPLOY_DEVICE; + errorCommandDeployWithoutDevice = + TelemetryEventName.MICROBIT_ERROR_DEPLOY_WITHOUT_DEVICE; + } + switch (message.type) { + case "complete": + telemetryAI.trackFeatureUsage(successCommandDeployDevice); + break; + case "no-device": + telemetryAI.trackFeatureUsage(errorCommandDeployWithoutDevice); + if (device === CONSTANTS.DEVICE_NAME.CPX) { + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.NO_DEVICE, + DialogResponses.HELP + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.HELP) { + const okAction = () => { + open(CONSTANTS.LINKS.HELP); + telemetryAI.trackFeatureUsage( + TelemetryEventName.CPX_CLICK_DIALOG_HELP_DEPLOY_TO_DEVICE + ); + }; + utils.showPrivacyModal( + okAction, + CONSTANTS.INFO.THIRD_PARTY_WEBSITE_ADAFRUIT + ); + } + }); + } else if (device === CONSTANTS.DEVICE_NAME.MICROBIT) { + vscode.window.showErrorMessage(CONSTANTS.ERROR.NO_DEVICE); + } + break; + case "low-python-version": + vscode.window.showErrorMessage( + CONSTANTS.ERROR.LOW_PYTHON_VERSION_FOR_MICROBIT_DEPLOYMENT + ); + break; + default: + console.log( + `Non-state JSON output from the process : ${message}` + ); + break; + } + }; + + const cpxDeployCodeToDevice = () => { + deployCode(CONSTANTS.DEVICE_NAME.CPX); + }; + + const microbitDeployCodeToDevice = () => { + deployCode(CONSTANTS.DEVICE_NAME.MICROBIT); + }; + const cpxDeployToDevice: vscode.Disposable = vscode.commands.registerCommand( "deviceSimulatorExpress.cpx.deployToDevice", () => { @@ -842,19 +836,32 @@ export async function activate(context: vscode.ExtensionContext) { } ); + const microbitDeployToDevice: vscode.Disposable = vscode.commands.registerCommand( + "deviceSimulatorExpress.microbit.deployToDevice", + () => { + telemetryAI.trackFeatureUsage( + TelemetryEventName.MICROBIT_COMMAND_DEPLOY_DEVICE + ); + telemetryAI.runWithLatencyMeasure( + microbitDeployCodeToDevice, + TelemetryEventName.MICROBIT_PERFORMANCE_DEPLOY_DEVICE + ); + } + ); + let serialMonitor: SerialMonitor | undefined; if (configFileCreated) { serialMonitor = SerialMonitor.getInstance(); context.subscriptions.push(serialMonitor); } - const cpxSelectSerialPort: vscode.Disposable = vscode.commands.registerCommand( - "deviceSimulatorExpress.cpx.selectSerialPort", + const selectSerialPort: vscode.Disposable = vscode.commands.registerCommand( + "deviceSimulatorExpress.common.selectSerialPort", () => { if (serialMonitor) { telemetryAI.runWithLatencyMeasure(() => { serialMonitor.selectSerialPort(null, null); - }, TelemetryEventName.CPX_COMMAND_SERIAL_MONITOR_CHOOSE_PORT); + }, TelemetryEventName.COMMAND_SERIAL_MONITOR_CHOOSE_PORT); } else { vscode.window.showErrorMessage( CONSTANTS.ERROR.NO_FOLDER_OPENED @@ -864,13 +871,13 @@ export async function activate(context: vscode.ExtensionContext) { } ); - const cpxOpenSerialMonitor: vscode.Disposable = vscode.commands.registerCommand( - "deviceSimulatorExpress.cpx.openSerialMonitor", + const openSerialMonitor: vscode.Disposable = vscode.commands.registerCommand( + "deviceSimulatorExpress.common.openSerialMonitor", () => { if (serialMonitor) { telemetryAI.runWithLatencyMeasure( serialMonitor.openSerialMonitor.bind(serialMonitor), - TelemetryEventName.CPX_COMMAND_SERIAL_MONITOR_OPEN + TelemetryEventName.COMMAND_SERIAL_MONITOR_OPEN ); } else { vscode.window.showErrorMessage( @@ -881,13 +888,13 @@ export async function activate(context: vscode.ExtensionContext) { } ); - const cpxChangeBaudRate: vscode.Disposable = vscode.commands.registerCommand( - "deviceSimulatorExpress.cpx.changeBaudRate", + const changeBaudRate: vscode.Disposable = vscode.commands.registerCommand( + "deviceSimulatorExpress.common.changeBaudRate", () => { if (serialMonitor) { telemetryAI.runWithLatencyMeasure( serialMonitor.changeBaudRate.bind(serialMonitor), - TelemetryEventName.CPX_COMMAND_SERIAL_MONITOR_BAUD_RATE + TelemetryEventName.COMMAND_SERIAL_MONITOR_BAUD_RATE ); } else { vscode.window.showErrorMessage( @@ -898,13 +905,13 @@ export async function activate(context: vscode.ExtensionContext) { } ); - const cpxCloseSerialMonitor: vscode.Disposable = vscode.commands.registerCommand( - "deviceSimulatorExpress.cpx.closeSerialMonitor", + const closeSerialMonitor: vscode.Disposable = vscode.commands.registerCommand( + "deviceSimulatorExpress.common.closeSerialMonitor", (port, showWarning = true) => { if (serialMonitor) { telemetryAI.runWithLatencyMeasure(() => { serialMonitor.closeSerialMonitor(port, showWarning); - }, TelemetryEventName.CPX_COMMAND_SERIAL_MONITOR_CLOSE); + }, TelemetryEventName.COMMAND_SERIAL_MONITOR_CLOSE); } else { vscode.window.showErrorMessage( CONSTANTS.ERROR.NO_FOLDER_OPENED @@ -920,7 +927,7 @@ export async function activate(context: vscode.ExtensionContext) { if (serialMonitor) { telemetryAI.runWithLatencyMeasure(() => { serialMonitor.closeSerialMonitor(port, showWarning); - }, TelemetryEventName.CPX_COMMAND_SERIAL_MONITOR_CLOSE); + }, TelemetryEventName.COMMAND_SERIAL_MONITOR_CLOSE); } else { vscode.window.showErrorMessage( CONSTANTS.ERROR.NO_FOLDER_OPENED @@ -958,7 +965,7 @@ export async function activate(context: vscode.ExtensionContext) { debuggerCommunicationService ); vscode.debug.registerDebugAdapterTrackerFactory( - "python", + LANGUAGE_VARS.PYTHON.ID, debugAdapterFactory ); // On Debug Session Start: Init comunication @@ -978,7 +985,7 @@ export async function activate(context: vscode.ExtensionContext) { new DebuggerCommunicationServer( currentPanel, utils.getServerPortConfig(), - currentActiveDevice + deviceSelectionService.getCurrentActiveDevice() ) ); @@ -990,7 +997,7 @@ export async function activate(context: vscode.ExtensionContext) { .getCurrentDebuggerServer() .setWebview(currentPanel); currentPanel.webview.postMessage({ - currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), command: "activate-play", }); } @@ -1023,7 +1030,7 @@ export async function activate(context: vscode.ExtensionContext) { if (currentPanel) { currentPanel.webview.postMessage({ command: "reset-state", - active_device: currentActiveDevice, + active_device: deviceSelectionService.getCurrentActiveDevice(), }); } } @@ -1032,7 +1039,7 @@ export async function activate(context: vscode.ExtensionContext) { const configsChanged = vscode.workspace.onDidChangeConfiguration( async () => { if (utils.checkConfig(CONFIG.CONFIG_ENV_ON_SWITCH)) { - pythonExecutablePath = await utils.setupEnv(context); + pythonExecutablePath = await setupService.setupEnv(context); } } ); @@ -1040,15 +1047,16 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( installDependencies, runSimulator, - cpxChangeBaudRate, - cpxCloseSerialMonitor, + changeBaudRate, + closeSerialMonitor, cpxDeployToDevice, cpxNewFile, - cpxOpenSerialMonitor, + openSerialMonitor, cpxOpenSimulator, - cpxSelectSerialPort, + selectSerialPort, microbitOpenSimulator, microbitNewFile, + microbitDeployToDevice, vscode.debug.registerDebugConfigurationProvider( CONSTANTS.DEBUG_CONFIGURATION_TYPE, simulatorDebugConfiguration @@ -1059,41 +1067,8 @@ export async function activate(context: vscode.ExtensionContext) { ); } -const getActivePythonFile = () => { - const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors; - const activeEditor = editors.find( - editor => editor.document.languageId === "python" - ); - if (activeEditor) { - currentTextDocument = activeEditor.document; - } - return activeEditor ? activeEditor.document.fileName : ""; -}; - -const updateCurrentFileIfPython = async ( - activeTextDocument: vscode.TextDocument | undefined, - currentPanel: vscode.WebviewPanel -) => { - if (activeTextDocument && activeTextDocument.languageId === "python") { - setPathAndSendMessage(currentPanel, activeTextDocument.fileName); - currentTextDocument = activeTextDocument; - } else if (currentFileAbsPath === "") { - setPathAndSendMessage(currentPanel, getActivePythonFile() || ""); - } - if ( - currentTextDocument && - utils.getActiveEditorFromPath(currentTextDocument.fileName) === - undefined - ) { - await vscode.window.showTextDocument( - currentTextDocument, - vscode.ViewColumn.One - ); - } -}; - const handleDebuggerTelemetry = () => { - switch (currentActiveDevice) { + switch (deviceSelectionService.getCurrentActiveDevice()) { case CONSTANTS.DEVICE_NAME.CPX: telemetryAI.trackFeatureUsage( TelemetryEventName.CPX_DEBUGGER_INIT_SUCCESS @@ -1110,7 +1085,7 @@ const handleDebuggerTelemetry = () => { }; const handleDebuggerFailTelemetry = () => { - switch (currentActiveDevice) { + switch (deviceSelectionService.getCurrentActiveDevice()) { case CONSTANTS.DEVICE_NAME.CPX: telemetryAI.trackFeatureUsage( TelemetryEventName.CPX_DEBUGGER_INIT_FAIL @@ -1127,7 +1102,7 @@ const handleDebuggerFailTelemetry = () => { }; const handleButtonPressTelemetry = (buttonState: any) => { - switch (currentActiveDevice) { + switch (deviceSelectionService.getCurrentActiveDevice()) { case CONSTANTS.DEVICE_NAME.CPX: handleCPXButtonPressTelemetry(buttonState); break; @@ -1140,7 +1115,7 @@ const handleButtonPressTelemetry = (buttonState: any) => { }; const handleGestureTelemetry = (sensorState: any) => { - switch (currentActiveDevice) { + switch (deviceSelectionService.getCurrentActiveDevice()) { case CONSTANTS.DEVICE_NAME.CPX: handleCPXGestureTelemetry(sensorState); break; @@ -1152,7 +1127,7 @@ const handleGestureTelemetry = (sensorState: any) => { }; const handleSensorTelemetry = (sensor: string) => { - switch (currentActiveDevice) { + switch (deviceSelectionService.getCurrentActiveDevice()) { case CONSTANTS.DEVICE_NAME.CPX: handleCPXSensorTelemetry(sensor); break; @@ -1277,7 +1252,7 @@ const handleMicrobitSensorTelemetry = (sensor: string) => { }; const handleNewFileErrorTelemetry = () => { - switch (currentActiveDevice) { + switch (deviceSelectionService.getCurrentActiveDevice()) { case CONSTANTS.DEVICE_NAME.CPX: telemetryAI.trackFeatureUsage( TelemetryEventName.CPX_ERROR_COMMAND_NEW_FILE @@ -1353,9 +1328,6 @@ function getWebviewContent(context: vscode.ExtensionContext) { `; } -function switchDevice(deviceName: string) { - currentActiveDevice = deviceName; -} // this method is called when your extension is deactivated export async function deactivate() { diff --git a/src/extension_utils/utils.ts b/src/extension_utils/utils.ts index ba51c6815..b7be3b71c 100644 --- a/src/extension_utils/utils.ts +++ b/src/extension_utils/utils.ts @@ -3,26 +3,20 @@ import * as cp from "child_process"; import * as fs from "fs"; -import * as open from "open"; -import * as os from "os"; import * as path from "path"; import * as util from "util"; import * as vscode from "vscode"; import { - CONFIG, CONSTANTS, CPX_CONFIG_FILE, DialogResponses, GLOBAL_ENV_VARS, - HELPER_FILES, SERVER_INFO, - USER_CODE_NAMES, - VERSIONS, } from "../constants"; import { CPXWorkspace } from "../cpxWorkspace"; import { DeviceContext } from "../deviceContext"; -const exec = util.promisify(cp.exec); +export const exec = util.promisify(cp.exec); const errorChannel = vscode.window.createOutputChannel( CONSTANTS.ERROR.INSTALLATION_ERROR @@ -41,13 +35,6 @@ export const getPathToScript = ( return scriptPath.fsPath; }; -export const validCodeFileName = (filePath: string) => { - return ( - filePath.endsWith(USER_CODE_NAMES.CODE_PY) || - filePath.endsWith(USER_CODE_NAMES.MAIN_PY) - ); -}; - export const showPrivacyModal = ( okAction: () => void, thirdPartyDisclaimer: string @@ -170,34 +157,6 @@ export function generateCPXConfig(): void { fs.writeFileSync(cpxConfigFilePath, JSON.stringify(cpxJson, null, 4)); } -export const isPipInstalled = async (pythonExecutablePath: string) => { - try { - const { stdout } = await executePythonCommand( - pythonExecutablePath, - " -m pip" - ); - return true; - } catch (err) { - vscode.window - .showErrorMessage( - CONSTANTS.ERROR.NO_PIP, - DialogResponses.INSTALL_PIP - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.INSTALL_PIP) { - const okAction = () => { - open(CONSTANTS.LINKS.DOWNLOAD_PIP); - }; - showPrivacyModal( - okAction, - CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PIP - ); - } - }); - return false; - } -}; - export const addVisibleTextEditorCallback = ( currentPanel: vscode.WebviewPanel, context: vscode.ExtensionContext @@ -272,393 +231,3 @@ export const getTelemetryState = () => { .getConfiguration() .get("telemetry.enableTelemetry", true); }; - -// Setup code starts - -export const checkIfVenv = async ( - context: vscode.ExtensionContext, - pythonExecutablePath: string -) => { - const venvCheckerPath: string = getPathToScript( - context, - CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, - HELPER_FILES.CHECK_IF_VENV_PY - ); - const { stdout } = await executePythonCommand( - pythonExecutablePath, - `"${venvCheckerPath}"` - ); - return stdout.trim() === "1"; -}; - -export const executePythonCommand = async ( - pythonExecutablePath: string, - command: string -) => { - return exec(`${createEscapedPath(pythonExecutablePath)} ${command}`); -}; - -export const validatePythonVersion = async (pythonExecutablePath: string) => { - const { stdout } = await executePythonCommand( - pythonExecutablePath, - "--version" - ); - if (stdout < VERSIONS.MIN_PY_VERSION) { - vscode.window - .showInformationMessage( - CONSTANTS.ERROR.INVALID_PYTHON_PATH, - DialogResponses.INSTALL_PYTHON - ) - .then((installChoice: vscode.MessageItem | undefined) => { - if (installChoice === DialogResponses.INSTALL_PYTHON) { - const okAction = () => { - open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); - }; - showPrivacyModal( - okAction, - CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PYTHON - ); - } - }); - return false; - } else { - return true; - } -}; - -export const hasVenv = async (context: vscode.ExtensionContext) => { - const pathToEnv: string = getPathToScript( - context, - CONSTANTS.FILESYSTEM.PYTHON_VENV_DIR - ); - - return fs.existsSync(pathToEnv); -}; - -export const promptInstallVenv = ( - context: vscode.ExtensionContext, - pythonExecutable: string, - pythonExecutableName: string -) => { - return vscode.window - .showInformationMessage( - CONSTANTS.INFO.INSTALL_PYTHON_VENV, - DialogResponses.YES, - DialogResponses.NO - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.YES) { - return installPythonVenv( - context, - pythonExecutable, - pythonExecutableName - ); - } else { - // return pythonExecutable, notifying the caller - // that the user was unwilling to create venv - // and by default, this will trigger the extension to - // try using pythonExecutable - return pythonExecutable; - } - }); -}; - -export const getPythonVenv = async ( - context: vscode.ExtensionContext, - pythonExecutableName: string -) => { - const subFolder = os.platform() === "win32" ? "Scripts" : "bin"; - - return getPathToScript( - context, - path.join(CONSTANTS.FILESYSTEM.PYTHON_VENV_DIR, subFolder), - pythonExecutableName - ); -}; - -export const installPythonVenv = async ( - context: vscode.ExtensionContext, - pythonExecutable: string, - pythonExecutableName: string -) => { - const pathToEnv: string = getPathToScript( - context, - CONSTANTS.FILESYSTEM.PYTHON_VENV_DIR - ); - - vscode.window.showInformationMessage(CONSTANTS.INFO.INSTALLING_PYTHON_VENV); - - const pythonPath: string = await getPythonVenv( - context, - pythonExecutableName - ); - - try { - // make venv - // run command to download dependencies to out/python_libs - await executePythonCommand(pythonExecutable, `-m venv "${pathToEnv}"`); - } catch (err) { - vscode.window - .showErrorMessage( - `Virtual environment for download could not be completed. Using original interpreter at: ${pythonExecutable}.`, - DialogResponses.READ_INSTALL_MD - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.READ_INSTALL_MD) { - open(CONSTANTS.LINKS.INSTALL); - } - }); - - console.error(err); - - return pythonExecutable; - } - - return installDependenciesWrapper(context, pythonPath, pythonExecutable); -}; - -export const areDependenciesInstalled = async ( - context: vscode.ExtensionContext, - pythonPath: string -) => { - const dependencyCheckerPath: string = getPathToScript( - context, - CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, - HELPER_FILES.CHECK_PYTHON_DEPENDENCIES - ); - try { - // python script will throw exception - // if not all dependencies are downloaded - const { stdout } = await executePythonCommand( - pythonPath, - `"${dependencyCheckerPath}"` - ); - - // output for debugging purposes - console.info(stdout); - return true; - } catch (err) { - return false; - } -}; - -export const installDependencies = async ( - context: vscode.ExtensionContext, - pythonPath: string -) => { - const requirementsPath: string = getPathToScript( - context, - CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, - "requirements.txt" - ); - - if (!isPipInstalled(pythonPath)) { - return false; - } - - try { - const { stdout } = await executePythonCommand( - pythonPath, - `-m pip install -r "${requirementsPath}"` - ); - - console.info(stdout); - vscode.window.showInformationMessage(CONSTANTS.INFO.SUCCESSFUL_INSTALL); - return true; - } catch (err) { - return false; - } -}; - -export const installDependenciesWrapper = async ( - context: vscode.ExtensionContext, - pythonPath: string, - backupPythonPath: string = "" -) => { - let errMessage = CONSTANTS.ERROR.DEPENDENCY_DOWNLOAD_ERROR; - if (backupPythonPath !== "") { - errMessage = `${errMessage} Using original interpreter at: ${backupPythonPath}.`; - } - if (!(await installDependencies(context, pythonPath))) { - vscode.window - .showErrorMessage( - CONSTANTS.ERROR.DEPENDENCY_DOWNLOAD_ERROR, - DialogResponses.READ_INSTALL_MD - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.READ_INSTALL_MD) { - open(CONSTANTS.LINKS.INSTALL); - } - }); - return backupPythonPath; - } - return pythonPath; -}; -export const getCurrentpythonExecutablePath = async () => { - let originalpythonExecutablePath = ""; - - // try to get name from interpreter - try { - originalpythonExecutablePath = getConfig(CONFIG.PYTHON_PATH); - } catch (err) { - originalpythonExecutablePath = GLOBAL_ENV_VARS.PYTHON; - } - - if ( - originalpythonExecutablePath === GLOBAL_ENV_VARS.PYTHON || - originalpythonExecutablePath === "" - ) { - try { - const { stdout } = await executePythonCommand( - GLOBAL_ENV_VARS.PYTHON, - `-c "import sys; print(sys.executable)"` - ); - originalpythonExecutablePath = stdout.trim(); - } catch (err) { - vscode.window - .showErrorMessage( - CONSTANTS.ERROR.NO_PYTHON_PATH, - DialogResponses.INSTALL_PYTHON - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.INSTALL_PYTHON) { - const okAction = () => { - open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); - }; - showPrivacyModal( - okAction, - CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PYTHON - ); - } - }); - - // no python installed, cannot get path - return ""; - } - } - // fix path to be absolute - if (!path.isAbsolute(originalpythonExecutablePath)) { - originalpythonExecutablePath = path.join( - vscode.workspace.rootPath, - originalpythonExecutablePath - ); - } - - if (!fs.existsSync(originalpythonExecutablePath)) { - await vscode.window.showErrorMessage(CONSTANTS.ERROR.BAD_PYTHON_PATH); - return ""; - } - - if (!(await validatePythonVersion(originalpythonExecutablePath))) { - return ""; - } - - return originalpythonExecutablePath; -}; -export const setupEnv = async ( - context: vscode.ExtensionContext, - needsResponse: boolean = false -) => { - const originalpythonExecutablePath = await getCurrentpythonExecutablePath(); - let pythonExecutablePath = originalpythonExecutablePath; - let pythonExecutableName: string = - os.platform() === "win32" - ? HELPER_FILES.PYTHON_EXE - : HELPER_FILES.PYTHON; - - if (!(await areDependenciesInstalled(context, pythonExecutablePath))) { - // environment needs to install dependencies - if (!(await checkIfVenv(context, pythonExecutablePath))) { - const pythonExecutablePathVenv = await getPythonVenv( - context, - pythonExecutableName - ); - if (await hasVenv(context)) { - // venv in extention exists with wrong dependencies - if ( - !(await areDependenciesInstalled( - context, - pythonExecutablePathVenv - )) - ) { - pythonExecutablePath = await installDependenciesWrapper( - context, - pythonExecutablePathVenv, - pythonExecutablePath - ); - } else { - pythonExecutablePath = pythonExecutablePathVenv; - } - } else { - pythonExecutablePath = await promptInstallVenv( - context, - originalpythonExecutablePath, - pythonExecutableName - ); - } - - if (pythonExecutablePath === pythonExecutablePathVenv) { - vscode.window.showInformationMessage( - CONSTANTS.INFO.UPDATED_TO_EXTENSION_VENV - ); - vscode.workspace - .getConfiguration() - .update(CONFIG.PYTHON_PATH, pythonExecutablePath); - } - } - if (pythonExecutablePath === originalpythonExecutablePath) { - // going with original interpreter, either because - // already in venv or error in creating custom venv - if (checkConfig(CONFIG.SHOW_DEPENDENCY_INSTALL)) { - await vscode.window - .showInformationMessage( - CONSTANTS.INFO.INSTALL_PYTHON_DEPS, - DialogResponses.INSTALL_NOW, - DialogResponses.DONT_INSTALL - ) - .then( - async ( - installChoice: vscode.MessageItem | undefined - ) => { - if (installChoice === DialogResponses.INSTALL_NOW) { - await installDependenciesWrapper( - context, - pythonExecutablePath - ); - } else { - await vscode.window - .showInformationMessage( - CONSTANTS.INFO.ARE_YOU_SURE, - DialogResponses.INSTALL_NOW, - DialogResponses.DONT_INSTALL - ) - .then( - async ( - installChoice2: - | vscode.MessageItem - | undefined - ) => { - if ( - installChoice2 === - DialogResponses.INSTALL_NOW - ) { - await installDependenciesWrapper( - context, - pythonExecutablePath - ); - } - } - ); - } - } - ); - } - } - } else if (needsResponse) { - vscode.window.showInformationMessage( - CONSTANTS.INFO.ALREADY_SUCCESSFUL_INSTALL - ); - } - - return pythonExecutablePath; -}; diff --git a/src/latest_release_note.ts b/src/latest_release_note.ts index 569e1f3c5..a4e1ddabd 100644 --- a/src/latest_release_note.ts +++ b/src/latest_release_note.ts @@ -1,5 +1,5 @@ // TODO: find a better way of loading html into a string -export const LATEST_RELEASE_NOTE = `

Device Simulator Express Release Notes πŸ‘©πŸΎβ€πŸ’» πŸ‘¨πŸΎβ€πŸ’» (Mar. 3, 2020)

+export const LATEST_RELEASE_NOTE = `

Device Simulator Express Release Notes πŸ‘©πŸΎβ€πŸ’» πŸ‘¨πŸΎβ€πŸ’» (Feb. 27, 2020)

Welcome to the first update to the Device Simulator Express! Please feel free to enable our feature flag in Settings diff --git a/src/process_user_code.py b/src/process_user_code.py index 3349ce0a5..4201acdd1 100644 --- a/src/process_user_code.py +++ b/src/process_user_code.py @@ -84,7 +84,7 @@ def handle_user_prints(): def execute_user_code(abs_path_to_code_file): cpx._Express__abs_path_to_code_file = abs_path_to_code_file # Execute the user's code.py file - with open(abs_path_to_code_file) as user_code_file: + with open(abs_path_to_code_file, encoding="utf8") as user_code_file: user_code = user_code_file.read() try: codeObj = compile(user_code, abs_path_to_code_file, CONSTANTS.EXEC_COMMAND) diff --git a/src/python_constants.py b/src/python_constants.py index 1e8442d38..a5f55bc8c 100644 --- a/src/python_constants.py +++ b/src/python_constants.py @@ -26,6 +26,7 @@ NO_CPX_DETECTED_ERROR_DETAIL = ( "Could not find drive with name 'CIRCUITPYTHON'. Detected OS: {}" ) +NO_MICROBIT_DETECTED_ERROR_TITLE = "No micro:bit detected" NOT_SUPPORTED_OS = 'The OS "{}" not supported.' NOT_IMPLEMENTED_ERROR = "This method is not implemented by the simulator" diff --git a/src/requirements.txt b/src/requirements.txt index 0708fa17c..3ed651739 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,7 +1,7 @@ -black==19.10b0 playsound==1.2.2 -pytest==5.0.1 applicationinsights==0.11.9 python-socketio==4.3.1 requests==2.22.0 pywin32==227; platform_system == "Windows" +PyObjC; platform_system == "darwin" +uflash==1.3.0 \ No newline at end of file diff --git a/src/serialMonitor.ts b/src/serialMonitor.ts index 2fd01588f..17144eeb6 100644 --- a/src/serialMonitor.ts +++ b/src/serialMonitor.ts @@ -77,7 +77,7 @@ export class SerialMonitor implements vscode.Disposable { STATUS_BAR_PRIORITY.PORT ); this._portsStatusBar.command = - "deviceSimulatorExpress.cpx.selectSerialPort"; + "deviceSimulatorExpress.common.selectSerialPort"; this._portsStatusBar.tooltip = "Select Serial Port"; this._portsStatusBar.show(); @@ -86,7 +86,7 @@ export class SerialMonitor implements vscode.Disposable { STATUS_BAR_PRIORITY.OPEN_PORT ); this._openPortStatusBar.command = - "deviceSimulatorExpress.cpx.openSerialMonitor"; + "deviceSimulatorExpress.common.openSerialMonitor"; this._openPortStatusBar.text = `$(plug)`; this._openPortStatusBar.tooltip = "Open Serial Monitor"; this._openPortStatusBar.show(); @@ -96,7 +96,7 @@ export class SerialMonitor implements vscode.Disposable { STATUS_BAR_PRIORITY.BAUD_RATE ); this._baudRateStatusBar.command = - "deviceSimulatorExpress.cpx.changeBaudRate"; + "deviceSimulatorExpress.common.changeBaudRate"; this._baudRateStatusBar.tooltip = "Baud Rate"; this._baudRateStatusBar.text = defaultBaudRate.toString(); this.updatePortListStatus(null); @@ -281,13 +281,13 @@ export class SerialMonitor implements vscode.Disposable { private updatePortStatus(isOpened: boolean) { if (isOpened) { this._openPortStatusBar.command = - "deviceSimulatorExpress.cpx.closeSerialMonitor"; + "deviceSimulatorExpress.common.closeSerialMonitor"; this._openPortStatusBar.text = `$(x)`; this._openPortStatusBar.tooltip = "Close Serial Monitor"; this._baudRateStatusBar.show(); } else { this._openPortStatusBar.command = - "deviceSimulatorExpress.cpx.openSerialMonitor"; + "deviceSimulatorExpress.common.openSerialMonitor"; this._openPortStatusBar.text = `$(plug)`; this._openPortStatusBar.tooltip = "Open Serial Monitor"; this._baudRateStatusBar.hide(); diff --git a/src/service/deviceSelectionService.ts b/src/service/deviceSelectionService.ts new file mode 100644 index 000000000..fd37ab6ed --- /dev/null +++ b/src/service/deviceSelectionService.ts @@ -0,0 +1,12 @@ +import { DEFAULT_DEVICE } from "../constants"; + +export class DeviceSelectionService { + private currentActiveDevice: string = DEFAULT_DEVICE; + + public getCurrentActiveDevice(): string { + return this.currentActiveDevice; + } + public setCurrentActiveDevice(newActiveDevice: string) { + this.currentActiveDevice = newActiveDevice; + } +} diff --git a/src/service/fileSelectionService.ts b/src/service/fileSelectionService.ts new file mode 100644 index 000000000..11f901ed9 --- /dev/null +++ b/src/service/fileSelectionService.ts @@ -0,0 +1,95 @@ +import * as vscode from "vscode"; +import { LANGUAGE_VARS } from "../constants"; +import { VSCODE_MESSAGES_TO_WEBVIEW } from "../view/constants"; +import { DeviceSelectionService } from "./deviceSelectionService"; +import { MessagingService } from "./messagingService"; + +export class FileSelectionService { + private currentFileAbsPath: string = ""; + private currentTextDocument: vscode.TextDocument; + private messagingService: MessagingService; + + constructor(messagingService: MessagingService) { + this.messagingService = messagingService; + } + + public getCurrentFileAbsPath() { + return this.currentFileAbsPath; + } + public getCurrentTextDocument() { + return this.currentTextDocument; + } + public updateCurrentFileFromTextFile = async ( + activeTextDocument: vscode.TextDocument | undefined + ) => { + if (activeTextDocument) { + await this.updateCurrentFileFromEditor({ + document: activeTextDocument, + } as vscode.TextEditor); + } else { + return; + } + }; + public updateCurrentFileFromEditor = async ( + activeTextDocument: vscode.TextEditor | undefined + ) => { + if ( + activeTextDocument && + activeTextDocument.document && + activeTextDocument.document.languageId === "python" + ) { + this.setPathAndSendMessage(activeTextDocument.document.fileName); + this.currentTextDocument = activeTextDocument.document; + } else if (this.currentFileAbsPath === "") { + this.setPathAndSendMessage(this.getActivePythonFile() || ""); + } + if ( + this.currentTextDocument && + this.getActiveEditorFromPath(this.currentTextDocument.fileName) === + undefined + ) { + await vscode.window.showTextDocument( + this.currentTextDocument, + vscode.ViewColumn.One + ); + } + }; + public findCurrentTextDocument() { + if (this.currentFileAbsPath) { + const foundDocument = this.getActiveEditorFromPath( + this.currentFileAbsPath + ); + if (foundDocument !== undefined) { + this.currentTextDocument = foundDocument; + } + } + } + + public setPathAndSendMessage = (newFilePath: string) => { + this.currentFileAbsPath = newFilePath; + this.messagingService.sendMessageToWebview( + VSCODE_MESSAGES_TO_WEBVIEW.CURRENT_FILE, + { + running_file: newFilePath, + } + ); + }; + private getActiveEditorFromPath = ( + filePath: string + ): vscode.TextDocument => { + const activeEditor = vscode.window.visibleTextEditors.find( + (editor: vscode.TextEditor) => editor.document.fileName === filePath + ); + return activeEditor ? activeEditor.document : undefined; + }; + private getActivePythonFile = () => { + const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors; + const activeEditor = editors.find( + editor => editor.document.languageId === LANGUAGE_VARS.PYTHON.ID + ); + if (activeEditor) { + this.currentTextDocument = activeEditor.document; + } + return activeEditor ? activeEditor.document.fileName : ""; + }; +} diff --git a/src/service/messagingService.ts b/src/service/messagingService.ts index 9e37f20a8..9b88b1439 100644 --- a/src/service/messagingService.ts +++ b/src/service/messagingService.ts @@ -1,16 +1,25 @@ import { Webview } from "vscode"; import { VSCODE_MESSAGES_TO_WEBVIEW } from "../view/constants"; +import { DeviceSelectionService } from "./deviceSelectionService"; export class MessagingService { private currentWebviewTarget: Webview | undefined; + private deviceSelectionService: DeviceSelectionService; + constructor(deviceSelectionService: DeviceSelectionService) { + this.deviceSelectionService = deviceSelectionService; + } public setWebview(webview: Webview) { this.currentWebviewTarget = webview; } // Send a message to webview if it exists - public sendMessageToWebview(debugCommand: string, state: Object) { + public sendMessageToWebview(command: string, stateToSend: Object) { if (this.currentWebviewTarget) { - this.currentWebviewTarget.postMessage({ command: debugCommand }); + this.currentWebviewTarget.postMessage({ + command, + active_device: this.deviceSelectionService.getCurrentActiveDevice(), + state: { ...stateToSend }, + }); } } public sendStartMessage() { diff --git a/src/service/setupService.ts b/src/service/setupService.ts new file mode 100644 index 000000000..569dbc943 --- /dev/null +++ b/src/service/setupService.ts @@ -0,0 +1,506 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as vscode from "vscode"; +import { + CONFIG, + CONSTANTS, + DialogResponses, + GLOBAL_ENV_VARS, + HELPER_FILES, + TelemetryEventName, + VERSIONS, +} from "../constants"; +import { + checkConfig, + createEscapedPath, + exec, + getConfig, + getPathToScript, + showPrivacyModal, +} from "../extension_utils/utils"; +import TelemetryAI from "../telemetry/telemetryAI"; + +export class SetupService { + private telemetryAI: TelemetryAI; + + constructor(telemetryAI: TelemetryAI) { + this.telemetryAI = telemetryAI; + } + + public setupEnv = async ( + context: vscode.ExtensionContext, + needsResponse: boolean = false + ) => { + const originalpythonExecutablePath = await this.getCurrentPythonExecutablePath(); + let pythonExecutablePath = originalpythonExecutablePath; + const pythonExecutableName: string = + os.platform() === "win32" + ? HELPER_FILES.PYTHON_EXE + : HELPER_FILES.PYTHON; + + if ( + !(await this.areDependenciesInstalled( + context, + pythonExecutablePath + )) + ) { + // environment needs to install dependencies + if (!(await this.checkIfVenv(context, pythonExecutablePath))) { + const pythonExecutablePathVenv = await this.getPythonVenv( + context, + pythonExecutableName + ); + if (await this.hasVenv(context)) { + // venv in extention exists with wrong dependencies + + if ( + !(await this.areDependenciesInstalled( + context, + pythonExecutablePathVenv + )) + ) { + pythonExecutablePath = await this.installDependenciesWrapper( + context, + pythonExecutablePathVenv, + pythonExecutablePath + ); + } else { + pythonExecutablePath = pythonExecutablePathVenv; + } + } else { + pythonExecutablePath = await this.promptInstallVenv( + context, + originalpythonExecutablePath, + pythonExecutableName + ); + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_INSTALL_VENV + ); + } + + if (pythonExecutablePath === pythonExecutablePathVenv) { + vscode.window.showInformationMessage( + CONSTANTS.INFO.UPDATED_TO_EXTENSION_VENV + ); + vscode.workspace + .getConfiguration() + .update(CONFIG.PYTHON_PATH, pythonExecutablePath); + } + } else { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_HAS_VENV + ); + } + if (pythonExecutablePath === originalpythonExecutablePath) { + // going with original interpreter, either because + // already in venv or error in creating custom venv + if (checkConfig(CONFIG.SHOW_DEPENDENCY_INSTALL)) { + await vscode.window + .showInformationMessage( + CONSTANTS.INFO.INSTALL_PYTHON_DEPS, + DialogResponses.INSTALL_NOW, + DialogResponses.DONT_INSTALL + ) + .then( + async ( + installChoice: vscode.MessageItem | undefined + ) => { + if ( + installChoice === + DialogResponses.INSTALL_NOW + ) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_ORIGINAL_INTERPRETER_DEP_INSTALL + ); + await this.installDependenciesWrapper( + context, + pythonExecutablePath + ); + } else { + await vscode.window + .showInformationMessage( + CONSTANTS.INFO.ARE_YOU_SURE, + DialogResponses.INSTALL_NOW, + DialogResponses.DONT_INSTALL + ) + .then( + async ( + installChoice2: + | vscode.MessageItem + | undefined + ) => { + if ( + installChoice2 === + DialogResponses.INSTALL_NOW + ) { + await this.installDependenciesWrapper( + context, + pythonExecutablePath + ); + } else { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_NO_DEPS_INSTALLED + ); + } + } + ); + } + } + ); + } + } + } else if (needsResponse) { + vscode.window.showInformationMessage( + CONSTANTS.INFO.ALREADY_SUCCESSFUL_INSTALL + ); + } + + return pythonExecutablePath; + }; + + public getCurrentPythonExecutablePath = async () => { + let originalpythonExecutablePath = ""; + + // try to get name from interpreter + try { + originalpythonExecutablePath = getConfig(CONFIG.PYTHON_PATH); + } catch (err) { + originalpythonExecutablePath = GLOBAL_ENV_VARS.PYTHON; + } + + if ( + originalpythonExecutablePath === GLOBAL_ENV_VARS.PYTHON || + originalpythonExecutablePath === "" + ) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_AUTO_RESOLVE_PYTHON_PATH + ); + try { + const { stdout } = await this.executePythonCommand( + GLOBAL_ENV_VARS.PYTHON, + `-c "import sys; print(sys.executable)"` + ); + originalpythonExecutablePath = stdout.trim(); + } catch (err) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_NO_PYTHON_PATH + ); + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.NO_PYTHON_PATH, + DialogResponses.INSTALL_PYTHON + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.INSTALL_PYTHON) { + const okAction = () => { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_DOWNLOAD_PYTHON + ); + open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); + }; + showPrivacyModal( + okAction, + CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PYTHON + ); + } + }); + // no python installed, cannot get path + return ""; + } + } + // fix path to be absolute + if (!path.isAbsolute(originalpythonExecutablePath)) { + originalpythonExecutablePath = path.join( + vscode.workspace.rootPath, + originalpythonExecutablePath + ); + } + + if (!fs.existsSync(originalpythonExecutablePath)) { + await vscode.window.showErrorMessage( + CONSTANTS.ERROR.BAD_PYTHON_PATH + ); + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_INVALID_PYTHON_INTERPRETER_PATH + ); + return ""; + } + + if (!(await this.validatePythonVersion(originalpythonExecutablePath))) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_INVALID_PYTHON_VER + ); + return ""; + } + + return originalpythonExecutablePath; + }; + + public isPipInstalled = async (pythonExecutablePath: string) => { + try { + const { stdout } = await this.executePythonCommand( + pythonExecutablePath, + " -m pip" + ); + return true; + } catch (err) { + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.NO_PIP, + DialogResponses.INSTALL_PIP + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.INSTALL_PIP) { + const okAction = () => { + open(CONSTANTS.LINKS.DOWNLOAD_PIP); + }; + showPrivacyModal( + okAction, + CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PIP + ); + } + }); + return false; + } + }; + + public checkIfVenv = async ( + context: vscode.ExtensionContext, + pythonExecutablePath: string + ) => { + const venvCheckerPath: string = getPathToScript( + context, + CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, + HELPER_FILES.CHECK_IF_VENV_PY + ); + const { stdout } = await this.executePythonCommand( + pythonExecutablePath, + `"${venvCheckerPath}"` + ); + return stdout.trim() === "1"; + }; + + public executePythonCommand = async ( + pythonExecutablePath: string, + command: string + ) => { + return exec(`${createEscapedPath(pythonExecutablePath)} ${command}`); + }; + + public validatePythonVersion = async (pythonExecutablePath: string) => { + const { stdout } = await this.executePythonCommand( + pythonExecutablePath, + "--version" + ); + if (stdout < VERSIONS.MIN_PY_VERSION) { + vscode.window + .showInformationMessage( + CONSTANTS.ERROR.INVALID_PYTHON_PATH, + DialogResponses.INSTALL_PYTHON + ) + .then((installChoice: vscode.MessageItem | undefined) => { + if (installChoice === DialogResponses.INSTALL_PYTHON) { + const okAction = () => { + open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); + }; + showPrivacyModal( + okAction, + CONSTANTS.INFO.THIRD_PARTY_WEBSITE_PYTHON + ); + } + }); + return false; + } else { + return true; + } + }; + + public hasVenv = async (context: vscode.ExtensionContext) => { + const pathToEnv: string = getPathToScript( + context, + CONSTANTS.FILESYSTEM.PYTHON_VENV_DIR + ); + + return fs.existsSync(pathToEnv); + }; + + public promptInstallVenv = ( + context: vscode.ExtensionContext, + pythonExecutable: string, + pythonExecutableName: string + ) => { + return vscode.window + .showInformationMessage( + CONSTANTS.INFO.INSTALL_PYTHON_VENV, + DialogResponses.YES, + DialogResponses.NO + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.YES) { + return this.installPythonVenv( + context, + pythonExecutable, + pythonExecutableName + ); + } else { + // return pythonExecutable, notifying the caller + // that the user was unwilling to create venv + // and by default, this will trigger the extension to + // try using pythonExecutable + return pythonExecutable; + } + }); + }; + + public getPythonVenv = async ( + context: vscode.ExtensionContext, + pythonExecutableName: string + ) => { + const subFolder = os.platform() === "win32" ? "Scripts" : "bin"; + + return getPathToScript( + context, + path.join(CONSTANTS.FILESYSTEM.PYTHON_VENV_DIR, subFolder), + pythonExecutableName + ); + }; + + public installPythonVenv = async ( + context: vscode.ExtensionContext, + pythonExecutable: string, + pythonExecutableName: string + ) => { + const pathToEnv: string = getPathToScript( + context, + CONSTANTS.FILESYSTEM.PYTHON_VENV_DIR + ); + + vscode.window.showInformationMessage( + CONSTANTS.INFO.INSTALLING_PYTHON_VENV + ); + + const pythonPath: string = await this.getPythonVenv( + context, + pythonExecutableName + ); + + try { + // make venv + // run command to download dependencies to out/python_libs + await this.executePythonCommand( + pythonExecutable, + `-m venv "${pathToEnv}"` + ); + } catch (err) { + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_VENV_CREATION_ERR + ); + vscode.window + .showErrorMessage( + `Virtual environment for download could not be completed. Using original interpreter at: ${pythonExecutable}.`, + DialogResponses.READ_INSTALL_MD + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.READ_INSTALL_MD) { + open(CONSTANTS.LINKS.INSTALL); + } + }); + + console.error(err); + + return pythonExecutable; + } + + return this.installDependenciesWrapper( + context, + pythonPath, + pythonExecutable + ); + }; + + public areDependenciesInstalled = async ( + context: vscode.ExtensionContext, + pythonPath: string + ) => { + const dependencyCheckerPath: string = getPathToScript( + context, + CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, + HELPER_FILES.CHECK_PYTHON_DEPENDENCIES + ); + try { + // python script will throw exception + // if not all dependencies are downloaded + const { stdout } = await this.executePythonCommand( + pythonPath, + `"${dependencyCheckerPath}"` + ); + + // output for debugging purposes + console.info(stdout); + return true; + } catch (err) { + return false; + } + }; + + public installDependencies = async ( + context: vscode.ExtensionContext, + pythonPath: string + ) => { + const requirementsPath: string = getPathToScript( + context, + CONSTANTS.FILESYSTEM.OUTPUT_DIRECTORY, + "requirements.txt" + ); + + if (!this.isPipInstalled(pythonPath)) { + this.telemetryAI.trackFeatureUsage(TelemetryEventName.SETUP_NO_PIP); + return false; + } + + try { + const { stdout } = await this.executePythonCommand( + pythonPath, + `-m pip install -r "${requirementsPath}"` + ); + + console.info(stdout); + vscode.window.showInformationMessage( + CONSTANTS.INFO.SUCCESSFUL_INSTALL + ); + return true; + } catch (err) { + return false; + } + }; + + public installDependenciesWrapper = async ( + context: vscode.ExtensionContext, + pythonPath: string, + backupPythonPath: string = "" + ) => { + let errMessage = CONSTANTS.ERROR.DEPENDENCY_DOWNLOAD_ERROR; + if (backupPythonPath !== "") { + errMessage = `${errMessage} Using original interpreter at: ${backupPythonPath}.`; + } + if (!(await this.installDependencies(context, pythonPath))) { + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.DEPENDENCY_DOWNLOAD_ERROR, + DialogResponses.READ_INSTALL_MD + ) + .then((selection: vscode.MessageItem | undefined) => { + if (selection === DialogResponses.READ_INSTALL_MD) { + open(CONSTANTS.LINKS.INSTALL); + } + }); + + this.telemetryAI.trackFeatureUsage( + TelemetryEventName.SETUP_DEP_INSTALL_FAIL + ); + return backupPythonPath; + } + return pythonPath; + }; +} diff --git a/src/simulatorDebugConfigurationProvider.ts b/src/simulatorDebugConfigurationProvider.ts index e0b711016..2b6725f22 100644 --- a/src/simulatorDebugConfigurationProvider.ts +++ b/src/simulatorDebugConfigurationProvider.ts @@ -3,12 +3,7 @@ import * as vscode from "vscode"; import { CONSTANTS, DialogResponses } from "./constants"; -import { - getServerPortConfig, - validCodeFileName, -} from "./extension_utils/utils"; - -let shouldShowInvalidFileNamePopup: boolean = true; +import { getServerPortConfig } from "./extension_utils/utils"; export class SimulatorDebugConfigurationProvider implements vscode.DebugConfigurationProvider { @@ -55,23 +50,6 @@ export class SimulatorDebugConfigurationProvider .then(() => { return undefined; // Abort launch }); - } else if ( - !validCodeFileName(currentFilePath) && - shouldShowInvalidFileNamePopup - ) { - vscode.window - .showInformationMessage( - CONSTANTS.INFO.INVALID_FILE_NAME_DEBUG, - ...[ - DialogResponses.DONT_SHOW, - DialogResponses.MESSAGE_UNDERSTOOD, - ] - ) - .then((selection: vscode.MessageItem | undefined) => { - if (selection === DialogResponses.DONT_SHOW) { - shouldShowInvalidFileNamePopup = false; - } - }); } // Set the new configuration type so the python debugger can take over config.type = "python"; diff --git a/src/templates/cpx_template.py b/src/templates/cpx_template.py index 3c118c7e9..e07eb906e 100644 --- a/src/templates/cpx_template.py +++ b/src/templates/cpx_template.py @@ -1,6 +1,4 @@ """ -Save your file as "code.py" or "main.py" to run on the actual device. - Getting started with CPX and CircuitPython intro on: https://learn.adafruit.com/circuitpython-made-easy-on-circuit-playground-express/circuit-playground-express-library diff --git a/src/view/components/cpx/CpxImage.tsx b/src/view/components/cpx/CpxImage.tsx index ee3d02201..8f57f3093 100644 --- a/src/view/components/cpx/CpxImage.tsx +++ b/src/view/components/cpx/CpxImage.tsx @@ -183,7 +183,7 @@ const initSvgStyle = (svgElement: HTMLElement, brightness: number): void => { const ab = outerBtn(165, SvgStyle.MB_HEIGHT - 15, "A+B"); const abtext = svg.child(ab.outer, "text", { - class: "sim-text", + class: "sim-text-outside", x: SvgStyle.BUTTON_TEXT_BASELINE, y: SvgStyle.MB_HEIGHT - 18, }) as SVGTextElement; diff --git a/src/view/components/cpx/Cpx_svg_style.tsx b/src/view/components/cpx/Cpx_svg_style.tsx index 72ba0e18a..4eff14480 100644 --- a/src/view/components/cpx/Cpx_svg_style.tsx +++ b/src/view/components/cpx/Cpx_svg_style.tsx @@ -141,6 +141,10 @@ export const SVG_STYLE = ` fill:#fff; pointer-events: none; user-select: none; } + .sim-text-outside{ + font-size:8px; + fill: var(--vscode-descriptionForeground); + } .sim-text.small { font-size:6px; } diff --git a/src/view/components/microbit/MicrobitImage.tsx b/src/view/components/microbit/MicrobitImage.tsx index 326d0a611..4ee638241 100644 --- a/src/view/components/microbit/MicrobitImage.tsx +++ b/src/view/components/microbit/MicrobitImage.tsx @@ -3,8 +3,8 @@ import * as React from "react"; import { VIEW_STATE } from "../../constants"; -import { ViewStateContext } from "../../context"; import CONSTANTS, { MICROBIT_BUTTON_STYLING_CLASSES } from "../../constants"; +import { ViewStateContext } from "../../context"; import "../../styles/Microbit.css"; import { IRefObject, MicrobitSvg } from "./Microbit_svg"; diff --git a/src/view/components/microbit/MicrobitSimulator.tsx b/src/view/components/microbit/MicrobitSimulator.tsx index c05611396..3d4258d64 100644 --- a/src/view/components/microbit/MicrobitSimulator.tsx +++ b/src/view/components/microbit/MicrobitSimulator.tsx @@ -10,7 +10,7 @@ import StopLogo from "../../svgs/stop_svg"; import { sendMessage } from "../../utils/MessageUtils"; import Dropdown from "../Dropdown"; import ActionBar from "../simulator/ActionBar"; -import { MicrobitImage, BUTTONS_KEYS } from "./MicrobitImage"; +import { BUTTONS_KEYS, MicrobitImage } from "./MicrobitImage"; const DEFAULT_MICROBIT_STATE: IMicrobitState = { leds: [ diff --git a/src/view/components/microbit/Microbit_svg.tsx b/src/view/components/microbit/Microbit_svg.tsx index e2e147412..0c5447fbd 100644 --- a/src/view/components/microbit/Microbit_svg.tsx +++ b/src/view/components/microbit/Microbit_svg.tsx @@ -1778,7 +1778,7 @@ export class MicrobitSvg extends React.Component { fill="#111" style={{ fill: "rgb(17, 17, 17)" }} /> - + A+B diff --git a/src/view/styles/Microbit.css b/src/view/styles/Microbit.css index b8a741988..f96655fa0 100644 --- a/src/view/styles/Microbit.css +++ b/src/view/styles/Microbit.css @@ -22,7 +22,10 @@ svg.sim.grayscale { .sim-button:active { fill: orange; } - +.sim-text-outside { + font-size: 25px; + fill: var(--vscode-descriptionForeground); +} .sim-board, .sim-display, sim-button {