├── .envrc ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ └── main.yml.not-ready ├── .gitignore ├── .tool-versions ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── images ├── BlinkaOnDark.png └── circuitpy-demo.gif ├── package-lock.json ├── package.json ├── requirements.txt ├── scripts ├── build-bindings.cmd ├── build-bindings.js ├── build-stubs.sh ├── build_stubs.py └── install-bindings.js ├── src ├── boards │ ├── board.ts │ └── boardManager.ts ├── container.ts ├── devicemanager │ └── deviceManager.ts ├── extension.ts ├── librarymanager │ ├── library.ts │ └── libraryManager.ts ├── project.ts ├── serialmonitor │ └── serialMonitor.ts └── test │ ├── runTest.ts │ └── suite │ ├── extension.test.ts │ └── index.ts └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck disable=SC2148 2 | use asdf; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: joedevivo 2 | custom: ["https://www.paypal.com/paypalme/joedevivo"] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | tags: 7 | - "v*" 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | include: 14 | - os: windows-latest 15 | platform: win32 16 | arch: x64 17 | npm_config_arch: x64 18 | - os: ubuntu-latest 19 | platform: linux 20 | arch: x64 21 | npm_config_arch: x64 22 | - os: ubuntu-latest 23 | platform: linux 24 | arch: armhf 25 | npm_config_arch: arm 26 | - os: macos-latest 27 | platform: darwin 28 | arch: x64 29 | npm_config_arch: x64 30 | - os: macos-latest 31 | platform: darwin 32 | arch: arm64 33 | npm_config_arch: arm64 34 | runs-on: ${{ matrix.os }} 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: 18 40 | cache: "npm" 41 | - uses: actions/setup-python@v5 42 | with: 43 | python-version: "3.11" 44 | - run: npm i -g npm@latest 45 | - run: npm --version 46 | - run: npm install 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | npm_config_arch: ${{ matrix.npm_config_arch }} 50 | - name: Electron Rebuild 51 | run: npm run electron-rebuild 52 | - name: Build stubs and board metadata 53 | run: ./scripts/build-stubs.sh 54 | shell: bash 55 | - name: Stay on target 56 | shell: pwsh 57 | run: echo "target=${{ matrix.platform }}-${{ matrix.arch }}" >> $env:GITHUB_ENV 58 | - name: Build Package 59 | run: npx vsce package --target ${{ env.target }} 60 | - uses: actions/upload-artifact@v4 61 | with: 62 | name: ${{ env.target }} 63 | path: "*.vsix" 64 | 65 | publish: 66 | runs-on: ubuntu-latest 67 | needs: build 68 | if: success() && startsWith( github.ref, 'refs/tags/') 69 | steps: 70 | - uses: actions/download-artifact@v4 71 | - run: npx vsce publish --packagePath $(find . -iname *.vsix) 72 | env: 73 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 74 | - run: npx ovsx publish --packagePath $(find . -iname *.vsix) -p $OPEN_VSX_TOKEN 75 | env: 76 | OPEN_VSX_TOKEN: ${{ secrets.OPEN_VSX_TOKEN }} 77 | -------------------------------------------------------------------------------- /.github/workflows/main.yml.not-ready: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: CI 3 | 4 | # Controls when the action will run. Triggers the workflow on push or pull request 5 | # events but only for the master branch 6 | on: 7 | push: 8 | branches: [ master, test-actions ] 9 | # pull_request: 10 | # branches: [ master ] 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # Get modules, rebuild for each platform 15 | node_modules: 16 | runs-on: macos-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Install Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 8.x 24 | - name: npm install 25 | run: npm install 26 | - name: persist serialport bindings 27 | uses: actions/upload-artifact@v1 28 | with: 29 | name: serialport 30 | path: node_modules/@serialport/bindings/bin 31 | - name: persist drivelist bindings 32 | uses: actions/upload-artifact@v1 33 | with: 34 | name: drivelist 35 | path: node_modules/drivelist/bin 36 | build: 37 | needs: node_modules 38 | strategy: 39 | # run one at a time, so we can keep the bindings for each platform 40 | max-parallel: 1 41 | matrix: 42 | os: [macos-latest, ubuntu-latest, windows-latest] 43 | runs-on: ${{ matrix.os }} 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v2 47 | - name: Install Node.js 48 | uses: actions/setup-node@v1 49 | with: 50 | node-version: 8.x 51 | - name: npm install 52 | run: npm install 53 | - name: download drivelist bindings 54 | uses: actions/download-artifact@v1 55 | with: 56 | name: drivelist 57 | path: node_modules/drivelist/bin 58 | - name: download serialport bindings 59 | uses: actions/download-artifact@v1 60 | with: 61 | name: serialport 62 | path: node_modules/@serialport/bindings/bin 63 | - name: electron-rebuild 64 | run: ./node_modules/.bin/electron-rebuild 65 | - name: persist serialport bindings 66 | uses: actions/upload-artifact@v1 67 | with: 68 | name: serialport 69 | path: node_modules/@serialport/bindings/bin 70 | - name: persist drivelist bindings 71 | uses: actions/upload-artifact@v1 72 | with: 73 | name: drivelist 74 | path: node_modules/drivelist/bin 75 | 76 | # Runs a set of commands using the runners shell 77 | #- name: Run a multi-line script 78 | # run: | 79 | # echo Add other actions to build, 80 | # echo test, and deploy your project. 81 | package: 82 | needs: build 83 | runs-on: macos-latest 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v2 87 | - name: download previous bindings 88 | uses: actions/download-artifact@v1 89 | with: 90 | name: node_modules 91 | path: node_modules 92 | - name: show bindings 93 | run: ls -al ./node_modules/@serialport/bindings/bin 94 | - name: Install Node.js 95 | uses: actions/setup-node@v1 96 | with: 97 | node-version: 8.x 98 | - name: npm install 99 | run: npm install 100 | - name: download drivelist bindings 101 | uses: actions/download-artifact@v1 102 | with: 103 | name: drivelist 104 | path: node_modules/drivelist/bin 105 | - name: download serialport bindings 106 | uses: actions/download-artifact@v1 107 | with: 108 | name: serialport 109 | path: node_modules/@serialport/bindings/bin 110 | - name: VSCE Package 111 | run: ./node_modules/.bin/vsce package 112 | - name: Persist package 113 | uses: actions/upload-artifact@v1 114 | with: 115 | name: extension 116 | # figure out versioning here. 117 | path: vscode-circuitpython-0.0.2.vsix -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | vsc-extension-quickstart.md 6 | circuitpython 7 | # mypy 8 | .mypy_cache/ 9 | circup* 10 | .env/ 11 | /bindings* 12 | /stubs 13 | /boards 14 | new.diff 15 | .DS_Store 16 | /tmp 17 | .history 18 | build 19 | *.tar.gz 20 | .yarn 21 | yarn-error.log 22 | package -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 14.16.0 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode", 8 | "dbaeumer.vscode-eslint", 9 | "amodio.tsl-problem-matcher" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "${defaultBuildTask}" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "npm.packageManager": "npm", 12 | "editor.tabSize": 2, 13 | "cSpell.words": [ 14 | "ansi", 15 | "blinka", 16 | "Blinka", 17 | "drivelist", 18 | "pylance" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "lint", 22 | "problemMatcher": [ 23 | "$eslint-stylish" 24 | ], 25 | "label": "npm: lint", 26 | "detail": "eslint -c .eslintrc.json --ext .ts src" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | **/tsconfig.json 7 | **/.eslintrc.json 8 | **/*.map 9 | **/*.ts 10 | .env 11 | TODO.md 12 | .tool-versions 13 | circuitpython 14 | .github 15 | requirements.txt 16 | *.vsix 17 | Makefile 18 | scripts 19 | dist 20 | package 21 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.2.0.cjs 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-circuitpython" extension will be documented 4 | in this file. 5 | 6 | ## [0.2.0] 7 | - Merged [#123](https://github.com/joedevivo/vscode-circuitpython/pull/123) 8 | - Merged [#62](https://github.com/joedevivo/vscode-circuitpython/pull/62) 9 | - Merged [#122](https://github.com/joedevivo/vscode-circuitpython/pull/122) 10 | - Merged [#29](https://github.com/joedevivo/vscode-circuitpython/pull/29) 11 | - Upgraded dependencies 12 | - Fixed version info for .py files 13 | 14 | ## [0.1.20] 15 | - Circuit Python 8 16 | 17 | ## [0.1.19] 18 | - Update to Electron 18 19 | - Added `Open Board's CircuitPython.org` page for easy access to firmware downloads 20 | - Every release includes the latest boards. 21 | 22 | ## [0.1.18] 23 | - Drop `pylance` dependency 24 | - Publish on `open-vsx.org` 25 | 26 | ## [0.1.17] 27 | - Update Electron to 17.2.0 28 | - VSCode >= 1.66.0 29 | - Update dependabot alert on `plist` 30 | 31 | ## [0.1.16] 32 | - Fix [#72](https://github.com/joedevivo/vscode-circuitpython/issues/72) 33 | - Board Update 34 | - CircuitPython Update 35 | - Upgrade `serialport` dependency 36 | - Move to GitHub Actions for CI 37 | 38 | ## [0.1.15] 39 | - Electron Rebuild 13.1.7 40 | - Board Update 41 | - CircuitPython Update 42 | - Fix version detection in mpy files for CP7 43 | 44 | ## [0.1.14] 45 | 46 | - Added download for 7.x mpy bundle 47 | - Electron Rebuild 12.0.13 48 | 49 | ### Bug Fixes 50 | - Skips enabling serial monitor if native bindings aren't loaded 51 | - This prevents the extension from crashing on launch if the VSCode version of 52 | electron has changed or the bindings aren't available for your 53 | system/architecture 54 | - Uses `""` instead of `null` in the `python.analysis.extraPaths` setting if a 55 | board is not chosen. 56 | - Fixed issue that prevented the board stub at `python.analysis.extraPaths[0]` 57 | from changing on board selection. 58 | - Fixed an issue preventing extension activation when workspace doesn't contain a `lib` folder 59 | - Fixed error downloading new Adafruit Bundle if bundle storage contains a file, 60 | where it previously assumed directories only. 61 | ## [0.1.13] 62 | - Put the bindings in the correct folder, 0.1.12 is pretty much broken for everyone. 63 | ## [0.1.12] 64 | - Electron Rebuild 12.0.4 65 | 66 | ## [0.1.11] 67 | - Fixed [#38](https://github.com/joedevivo/vscode-circuitpython/pull/37) 68 | issue with Apple Silicon native bindings 69 | - Fixed [#42](https://github.com/joedevivo/vscode-circuitpython/pull/42) & 70 | [#44](https://github.com/joedevivo/vscode-circuitpython/pull/44) 71 | `boot_out.txt` was required. Still is to determine CircuitPython version, but 72 | we can live without that. 73 | 74 | ## [0.1.10] 75 | - Disable pylint by default 76 | - Opt in to pylance by default 77 | - Added link to [GitHub Discussions](https://github.com/joedevivo/vscode-circuitpython/discussions) for Q&A. 78 | 79 | 80 | ## [0.1.9] 81 | - Library management fix [#37](https://github.com/joedevivo/vscode-circuitpython/pull/37) 82 | - thanks @makermelissa! 83 | - Fixes edge case where extension crashes if bundle was never loaded 84 | - Updated boards for March 2021 85 | - Update dependencies 86 | 87 | ## [0.1.8] 88 | - all the fun of 0.1.7, but built with proper bindings 89 | 90 | ## ~~[0.1.7]~~ 91 | - Upgraded Electron to 11.2.1 92 | - Updated VSCode version requirement to 1.53.0 93 | - Feb 2021 board refresh 94 | 95 | ## [0.1.6] 96 | - Jan 2021 board refresh 97 | 98 | ## [0.1.5] 99 | - Fixed issue installing 6.x mpy stubs on computers that had previous versions 100 | 101 | ## [0.1.4] 102 | - Improved logic for generating board stubs 103 | 104 | ## [0.1.3] 105 | - New Azure Pipelie Build 106 | - Should resolve native binding issues 107 | - New Boards are added in the build from adafruit/circuitpython:main 108 | - TODO: Let the user choose their version of CircuitPython, with the boards supported by that version. 109 | 110 | ## [0.1.2] 111 | - Added `Use Ctrl-C to enter the REPL` on Serial Monitor Connect 112 | - New Boards 113 | - Radomir Dopieralski : Fluff M0 114 | - Alorium Technology, LLC : AloriumTech Evo M51 115 | - maholli : PyCubed 116 | - PJRC : Teensy 4.1 117 | - Makerdiary : Pitaya Go 118 | - Nordic Semiconductor : PCA10100 119 | - HiiBot : HiiBot BlueFi 120 | - Espressif : Saola 1 w/WROOM 121 | - Espressif : Saola 1 w/WROVER 122 | 123 | ## [0.1.1] 124 | - Updated stubs for Circuit Python 5.3.0 125 | - New Boards 126 | - NFC Copy Cat 127 | - ST STM32F746G Discovery - CPy 128 | - OpenMV-H7 R1 129 | - Nucleo H743ZI - CPy 130 | - Nucleo F767ZI - CPy 131 | - Nucleo F746zg - CPy 132 | - Simmel 133 | - Native bindings for Raspberry Pi/ARM 134 | 135 | ## [0.1.0] 136 | 137 | - Reworked internals to be less `static` 138 | - more robust autocomplete path handling 139 | - updated stubs for Circuit Python 5.1.0 140 | - new boards 141 | 142 | ## [0.0.5] 143 | 144 | - Removed dialog on serial monitor open 145 | - stores board info in settings.json, which has the effect of persisting your 146 | board choice for a project between sessions. 147 | - Added command to manually check for bundle update 148 | 149 | ## [0.0.4] 150 | 151 | - Refactored the serial monitor from an output channel to a terminal, allowing 152 | interaction with the Circuit Python REPL 153 | 154 | ## [0.0.3] 155 | 156 | - More board completions 157 | 158 | ## [0.0.2] 159 | 160 | - Rebuilt for Electron@7 161 | - Reimplemented circup features directly in the extension, see libraryManager.ts for details 162 | - moves older bundles to the Trash instead of deleting. 163 | - Fixed OS dependent paths ( removed direct calls to path.posix ) 164 | - In theory, native bindings should work for windows and mac 165 | - no linux support yet, but it's on the way. I need to streamline the very 166 | manual process it took to get these bindings done. 167 | 168 | ## [0.0.1] 169 | 170 | - Initial release 171 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Joe DeVivo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: find-native 2 | find-native: 3 | @find node_modules -type f -name "*.node" 2>/dev/null | grep -v "obj\.target" 4 | 5 | .PHONY: all 6 | all: 7 | @npm install 8 | @npm run electron-rebuild 9 | @./scripts/build-stubs.sh 10 | @npx @vscode/vsce package 11 | 12 | .PHONY: quick 13 | quick: 14 | @npm install 15 | @npx @vscode/vsce package 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-circuitpython README 2 | 3 | This extension aspires to bring your entire CircuitPython workflow into a single 4 | place in VSCode. 5 | 6 | Inspired by [Scott Hanselman's blog 7 | post](https://www.hanselman.com/blog/UsingVisualStudioCodeToProgramCircuitPythonWithAnAdaFruitNeoTrellisM4.aspx) 8 | and the [VSCode Arduino extension](https://github.com/Microsoft/vscode-arduino). 9 | 10 | ## Getting Started 11 | 12 | The extension will currently activate when any of the following occur: 13 | 14 | * workspace contains 15 | * `/code.py` 16 | * `/code.txt` 17 | * `/main.py` 18 | * `/main.txt` 19 | * `/boot_out.txt` 20 | * command run 21 | * `circuitpython.openSerialMonitor` 22 | * `circuitpython.selectSerialPort` 23 | * `circuitpython.closeSerialMonitor` 24 | 25 | Upon activation, the extension will check for the latest 26 | [Adafruit_CircuitPython_Bundle](https://github.com/adafruit/Adafruit_CircuitPython_Bundle) 27 | and download it if needed. It'll then load that library metadata into the 28 | workspace's state. You can also trigger this manually with `CircuitPython: Check 29 | for latest bundle`. 30 | 31 | After that you should be ready to use the following features. 32 | 33 | ## Features 34 | 35 | ### Library Management 36 | 37 | v0.0.2 introduced a [Circup](https://github.com/adafruit/circup) inspired 38 | library manager, with an emphasis on VSCode integration. It downloads new 39 | bundles automatically. 40 | 41 | You can use it with the following commands: 42 | 43 | * `CircuitPython: Show Available Libraries` 44 | This is every library in the Adafruit Bundle. Alphabetical, but 45 | installed libraries are grouped on top. Click an out of date library 46 | to update, click an uninstalled library to install it. 47 | * `CircuitPython: List Project Libraries` 48 | Lists what's in your project's lib. If anything is out of date, click 49 | it to update. 50 | * `CircuitPython: Reload Project Libraries` 51 | In case it's reporting incorrectly. This can happen if you modify the 52 | filesystem outside of vscode. 53 | * `CircuitPython: Update All Libraries` 54 | Equivalent of `circup update --all` 55 | * `CircuitPython: Check for latest bundle` 56 | Compares the bundle on disk to the latest github release, downloads the 57 | release if it's newer. 58 | 59 | ### Serial Console 60 | 61 | `Circuit Python: Open Serial Console` will prompt you for a serial port to 62 | connect to, then it will display the serial output form the board attached to 63 | that port. The port can be changed by clicking on it's path in the status bar. 64 | 65 | Hit `Ctrl-C` and any key to enter the Circuit Python REPL, and `Ctrl-D` to 66 | reload. 67 | 68 | Note: There are linux permissions issues with the serial console, but if you're 69 | on linux, you're probably used to that. 70 | 71 | It will also change your workspace's default `board.pyi` file for autocomplete 72 | to the one that matches the USB Vendor ID & Product ID. 73 | 74 | If you want to manually choose a different board, a list is available with the 75 | command `CircuitPython: Choose CircuitPython Board`, and also by clicking on the 76 | board name in the status bar. 77 | 78 | **NOTE FOR WINDOWS USERS**: I have seen trouble with the serial console, 79 | displaying anything at all. If that happens, try launching VSCode as an 80 | administrator and see if it works. I have even gotten it to work as a 81 | non-administrator after this, so perhaps running it as an admin stole the serial 82 | port from whatever was using it, and then whatever it was didn't grab it again. 83 | 84 | ### Auto Complete 85 | 86 | Automatically adds stubs for your specific board, the circuitpython standard 87 | library and all py source files in the adafruit bundle to your completion path. 88 | 89 | ### Demo 90 | 91 | ![Demo](images/circuitpy-demo.gif) 92 | 93 | ## Requirements 94 | 95 | ## Extension Settings 96 | 97 | ### Board Settings 98 | 99 | Board specific settings can be stored in a project's `.vscode/settings.json` 100 | file, which will default to this board. This is great for when opening up the 101 | CIRCUITPY drive as a vscode workspace, and will be automatically set every time 102 | you choose a board. 103 | 104 | You can also use this for projects you're working from on disk, with the intent 105 | of running on a specific board. 106 | 107 | You can also set these at a user level, although that's not the primary intent. 108 | If you do this, it will get overridden at the workspace level if you ever touch 109 | the choose board dropdown or open a serial monitor. 110 | 111 | I'd probably have restricted the scope to workspace if that was an option. 112 | 113 | `circuitpython.board.vid`: Vendor ID for the project's board 114 | `circuitpython.board.pid`: Product ID for the project's board 115 | `circuitpython.board.version`: Persisted for choosing the right mpy binaries 116 | 117 | ## Known Issues 118 | 119 | ## Release Notes 120 | 121 | See the [Changelog](CHANGELOG.md) 122 | -------------------------------------------------------------------------------- /images/BlinkaOnDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joedevivo/vscode-circuitpython/877f8fe487fa07b90540789dfbc3844a464e1f25/images/BlinkaOnDark.png -------------------------------------------------------------------------------- /images/circuitpy-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joedevivo/vscode-circuitpython/877f8fe487fa07b90540789dfbc3844a464e1f25/images/circuitpy-demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-circuitpython", 3 | "displayName": "CircuitPython", 4 | "description": "CircuitPython for Visual Studio Code", 5 | "version": "0.2.0", 6 | "publisher": "joedevivo", 7 | "license": "MIT", 8 | "qna": "https://github.com/joedevivo/vscode-circuitpython/discussions", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/joedevivo/vscode-circuitpython.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/joedevivo/vscode-circuitpython/issues" 15 | }, 16 | "engines": { 17 | "vscode": "^1.85.0" 18 | }, 19 | "icon": "images/BlinkaOnDark.png", 20 | "categories": [ 21 | "Programming Languages" 22 | ], 23 | "keywords": [ 24 | "iot", 25 | "adafruit", 26 | "circuitpython", 27 | "blinka", 28 | "python" 29 | ], 30 | "activationEvents": [ 31 | "workspaceContains:/code.py", 32 | "workspaceContains:/code.txt", 33 | "workspaceContains:/main.py", 34 | "workspaceContains:/main.txt", 35 | "workspaceContains:/boot_out.txt", 36 | "onCommand:circuitpython.openSerialMonitor", 37 | "onCommand:circuitpython.selectSerialPort", 38 | "onCommand:circuitpython.closeSerialMonitor" 39 | ], 40 | "main": "./out/extension.js", 41 | "contributes": { 42 | "commands": [ 43 | { 44 | "command": "circuitpython.selectSerialPort", 45 | "title": "CircuitPython: Select Serial Port" 46 | }, 47 | { 48 | "command": "circuitpython.openSerialMonitor", 49 | "title": "CircuitPython: Open Serial Monitor" 50 | }, 51 | { 52 | "command": "circuitpython.closeSerialMonitor", 53 | "title": "CircuitPython: Close Serial Monitor" 54 | }, 55 | { 56 | "command": "circuitpython.sendMessageToSerialPort", 57 | "title": "CircuitPython: Send Text to Serial Port" 58 | }, 59 | { 60 | "command": "circuitpython.selectBoard", 61 | "title": "CircuitPython: Choose CircuitPython Board" 62 | }, 63 | { 64 | "command": "circuitpython.openBoardSite", 65 | "title": "CircuitPython: Open Current Board's CircuitPython.org" 66 | }, 67 | { 68 | "command": "circuitpython.library.show", 69 | "title": "CircuitPython: Show Available Libraries" 70 | }, 71 | { 72 | "command": "circuitpython.library.list", 73 | "title": "CircuitPython: List Project Libraries" 74 | }, 75 | { 76 | "command": "circuitpython.library.reload", 77 | "title": "CircuitPython: Reload Project Libraries" 78 | }, 79 | { 80 | "command": "circuitpython.library.update", 81 | "title": "CircuitPython: Update All Libraries" 82 | }, 83 | { 84 | "command": "circuitpython.library.fetch", 85 | "title": "CircuitPython: Check for latest bundle" 86 | } 87 | ], 88 | "configuration": { 89 | "title": "Circuit Python", 90 | "properties": { 91 | "circuitpython.board.vid": { 92 | "type": [ 93 | "string", 94 | "null" 95 | ], 96 | "default": null, 97 | "description": "Vendor ID for the current board. Intended to be set at the workspace level.", 98 | "scope": "window" 99 | }, 100 | "circuitpython.board.pid": { 101 | "type": [ 102 | "string", 103 | "null" 104 | ], 105 | "default": null, 106 | "description": "Product ID for the current board. Intended to be set at the workspace level.", 107 | "scope": "window" 108 | }, 109 | "circuitpython.board.version": { 110 | "type": [ 111 | "string", 112 | "null" 113 | ], 114 | "default": null, 115 | "description": "Circuit Python version for the current board. Intended to be set at the workspace level.", 116 | "scope": "window" 117 | } 118 | } 119 | }, 120 | "keybindings": [ 121 | { 122 | "command": "circuitpython.openSerialMonitor", 123 | "key": "ctrl+alt+r", 124 | "mac": "cmd+alt+r" 125 | } 126 | ] 127 | }, 128 | "scripts": { 129 | "vscode:prepublish": "npm run compile", 130 | "test-compile": "tsc -p ./", 131 | "deploy": "vsce publish", 132 | "compile": "tsc -b ./", 133 | "lint": "eslint -c .eslintrc.json --ext .ts src", 134 | "watch": "tsc -watch -p ./", 135 | "pretest": "npm run compile && yarn npm lint", 136 | "test": "node ./out/test/runTest.js", 137 | "electron-rebuild": "electron-rebuild -v 25.9.7", 138 | "build-bindings": "node ./scripts/build-bindings.js", 139 | "install-bindings": "node ./scripts/install-bindings.js" 140 | }, 141 | "extensionDependencies": [ 142 | "ms-python.python" 143 | ], 144 | "devDependencies": { 145 | "@electron/rebuild": "^3.4.1", 146 | "@mapbox/node-pre-gyp": "^1.0.5", 147 | "@types/drivelist": "6.4.4", 148 | "@types/glob": "^7.1.4", 149 | "@types/lodash": "^4.14.202", 150 | "@types/mocha": "^10.0.6", 151 | "@types/node": "18.x", 152 | "@types/serialport": "^8.0.2", 153 | "@types/vscode": "^1.85.0", 154 | "@typescript-eslint/eslint-plugin": "^5.62.0", 155 | "@typescript-eslint/parser": "^5.62.0", 156 | "@vscode/test-electron": "^2.1.3", 157 | "@vscode/vsce": "^2.22.0", 158 | "ansi-regex": "^6.0.1", 159 | "electron": "25.9.7", 160 | "electron-builder": "^24.9.1", 161 | "eslint": "^8.56.0", 162 | "glob": "^7.2.3", 163 | "minimist": "^1.2.8", 164 | "mocha": "^10.2.0", 165 | "node-abi": "^3.52.0", 166 | "node-gyp": "^10.0.1", 167 | "tar": "^6.2.0", 168 | "ts-loader": "^9.5.1", 169 | "typed-rest-client": "^1.8.11", 170 | "typescript": "^4.9.5", 171 | "typescript-http-client": "^0.10.4" 172 | }, 173 | "dependencies": { 174 | "@serialport/bindings-cpp": "^12.0.1", 175 | "@serialport/bindings-interface": "^1.2.2", 176 | "axios": "^1.6.3", 177 | "drivelist": "^11.1.0", 178 | "fs-extra": "^8.1.0", 179 | "globby": "^11.0.4", 180 | "lodash": "^4.17.21", 181 | "semver": "^7.5.4", 182 | "serialport": "^12.0.0", 183 | "shelljs": "^0.8.5", 184 | "trash": "^6.1.1", 185 | "typescript-string-operations": "^1.4.1", 186 | "unzipper": "^0.10.11" 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joedevivo/vscode-circuitpython/877f8fe487fa07b90540789dfbc3844a464e1f25/requirements.txt -------------------------------------------------------------------------------- /scripts/build-bindings.cmd: -------------------------------------------------------------------------------- 1 | echo off 2 | mkdir -p bindings 3 | SET electron="13.1.7" 4 | Rem ./scripts/build-bindings.sh $(System.DefaultWorkingDirectory) 5 | Rem where I want bindings to end up: 6 | Rem $(System.DefaultWorkingDirectory)/node_modules/$(module)/lib/binding/node-v$(abi)-$(platform)-$(arch)/bindings.node 7 | set working=%cd% 8 | 9 | cd node_modules\%1 10 | for %%e in (%electron%) do (call %working%\node_modules\.bin\prebuild.cmd -t %%e% -r electron) 11 | Rem $working/node_modules/.bin/prebuild.cmd -t $e -r electron 12 | Rem done 13 | 14 | Rem COPY prebuilds %working%\bindings 15 | cd %working% 16 | xcopy node_modules\%1\prebuilds bindings /s /e /Y -------------------------------------------------------------------------------- /scripts/build-bindings.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const nodeAbi = require('node-abi'); 3 | const globby = require('globby'); 4 | var abi = nodeAbi.getAbi('13.1.7', 'electron'); 5 | var dir = `node-v${abi}-${process.platform}-${process.arch}`; 6 | console.log(`Building bindings for ${dir}`); 7 | var fs = require('fs'); 8 | let re = new RegExp('node_modules/(.*)/build/Release/(.*).node', ''); 9 | var path=require('path'); 10 | (async () => { 11 | var filez = await globby('node_modules/**/build/Release/*.node'); 12 | filez.forEach(f => { 13 | console.log(`Found ${f}`); 14 | dest = f.replace(re, `bindings/node_modules/$1/lib/binding/${dir}/$2.node`); 15 | parent = path.dirname(dest); 16 | console.log(`Moving ${parent}`); 17 | console.log(` to ${dest}`); 18 | fs.mkdirSync(parent, {recursive: true}); 19 | fs.renameSync(f, dest); 20 | }); 21 | })(); 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /scripts/build-stubs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ( 4 | # Current dir should be the root of the repo 5 | cd $(dirname $0)/.. 6 | 7 | git clone --depth 1 --branch main https://github.com/adafruit/circuitpython.git 8 | 9 | cd circuitpython 10 | git submodule init 11 | git submodule update extmod/ulab 12 | 13 | # Use a venv for these 14 | # Using this name so circuitpython repo already gitignores it 15 | python3 -m venv .venv/ 16 | . .venv/bin/activate 17 | 18 | # `make stubs` in circuitpython 19 | pip3 install wheel # required on debian buster for some reason 20 | pip3 install -r requirements-doc.txt 21 | make stubs 22 | if [ -d ../stubs ]; then 23 | mv circuitpython-stubs/* ../stubs/ 24 | else 25 | # if stubs already exists, this would get interpreted as "move circuitpython-stubs *into* stubs" 26 | # hence the "if". Friendlier than the alternative, `rm -rf stubs` 27 | mv circuitpython-stubs/ ../stubs 28 | fi 29 | cd .. 30 | 31 | # scripts/build_stubs.py in this repo for board stubs 32 | python3 ./scripts/build_stubs.py 33 | rm -rf stubs/board 34 | 35 | # was crashing on `deactivate`, but guess what?! We're in parenthesis, so 36 | # it's a subshell. venv will go away when that subshell exits, which is, 37 | # wait for it.... now! 38 | ) 39 | -------------------------------------------------------------------------------- /scripts/build_stubs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This is a script for using circuitpython's repo to make pyi files for each board type. 3 | # These need to be bundled with the extension, which means that adding new boards is still 4 | # a new release of the extension. 5 | 6 | import json 7 | import pathlib 8 | import re 9 | 10 | 11 | def main(): 12 | repo_root = pathlib.Path(__file__).resolve().parent.parent 13 | # First thing we want to do is store in memory, the contents of 14 | # ./stubs/board/__init__.py so we can append it (partially) to 15 | # every other board. 16 | # See [Issue #26](https://github.com/joedevivo/vscode-circuitpython/issues/26) 17 | # for more on this. 18 | board_stub = repo_root / "stubs" / "board" / "__init__.pyi" 19 | generic_stubs = parse_generic_stub(board_stub) 20 | 21 | circuitpy_repo_root = repo_root / "circuitpython" 22 | boards = process_boards(repo_root, circuitpy_repo_root, generic_stubs) 23 | 24 | json_file = repo_root / "boards" / "metadata.json" 25 | with open(json_file, "w") as metadata: 26 | json.dump(boards, metadata) 27 | 28 | 29 | def parse_generic_stub(board_stub): 30 | generic_stubs = {} 31 | def_re = re.compile(r"def ([^\(]*)\(.*") 32 | with open(board_stub) as stub: 33 | stubs = stub.readlines() 34 | 35 | # Find the first line number and name of each definition 36 | f = [] 37 | names = [] 38 | for i, s in enumerate(stubs): 39 | match = def_re.match(s) 40 | if match is not None: 41 | f.append(i) 42 | names.append(match[1]) 43 | f.append(len(stubs)) 44 | 45 | # Iterate the line ranges 46 | for name, start, end in zip(names, f, f[1:]): 47 | generic_stubs[name] = "".join(stubs[start:end]) 48 | return generic_stubs 49 | 50 | 51 | def normalize_vid_pid(vid_or_pid: str): 52 | """Make a hex string all uppercase except for the 0x.""" 53 | return vid_or_pid.upper().replace("0X", "0x") 54 | 55 | 56 | _PIN_DEF_RE = re.compile( 57 | r"\s*{\s*MP_ROM_QSTR\(MP_QSTR_(?P[^\)]*)\)\s*,\s*MP_ROM_PTR\((?P[^\)]*)\).*" 58 | ) 59 | 60 | 61 | def parse_pins(generic_stubs, pins: pathlib.Path, board_stubs): 62 | imports = set() 63 | stub_lines = [] 64 | with open(pins) as p: 65 | for line in p: 66 | pin = _PIN_DEF_RE.match(line) 67 | if pin is None: 68 | continue 69 | pin_name = pin.group("name") 70 | if pin_name in generic_stubs: 71 | board_stubs[pin_name] = generic_stubs[pin_name] 72 | if "busio" in generic_stubs[pin_name]: 73 | imports.add("busio") 74 | continue 75 | 76 | pin_type = None 77 | 78 | # sometimes we can guess better based on the value 79 | pin_value = pin.group("value") 80 | if pin_value == "&displays[0].epaper_display": 81 | imports.add("displayio") 82 | pin_type = "displayio.EPaperDisplay" 83 | elif pin_value == "&displays[0].display": 84 | imports.add("displayio") 85 | pin_type = "displayio.Display" 86 | elif pin_value.startswith("&pin_"): 87 | imports.add("microcontroller") 88 | pin_type = "microcontroller.Pin" 89 | 90 | if pin_type is None: 91 | imports.add("typing") 92 | pin_type = "typing.Any" 93 | 94 | stub_lines.append("{0}: {1} = ...\n".format(pin_name, pin_type)) 95 | 96 | imports_string = "".join("import %s\n" % x for x in sorted(imports)) 97 | 98 | stubs_string = "".join(stub_lines) 99 | return imports_string, stubs_string 100 | 101 | 102 | # now, while we build the actual board stubs, replace any line that starts with ` $name:` with value 103 | 104 | 105 | def process_boards(repo_root, circuitpy_repo_root, generic_stubs): 106 | boards = [] 107 | 108 | board_configs = circuitpy_repo_root.glob("ports/*/boards/*/mpconfigboard.mk") 109 | for config in board_configs: 110 | b = config.parent 111 | site_path = b.stem 112 | 113 | print(config) 114 | pins = b / "pins.c" 115 | if not config.is_file() or not pins.is_file(): 116 | continue 117 | 118 | usb_vid = "" 119 | usb_pid = "" 120 | usb_product = "" 121 | usb_manufacturer = "" 122 | with open(config) as conf: 123 | for line in conf: 124 | if line.startswith("USB_VID"): 125 | usb_vid = line.split("=")[1].split("#")[0].strip('" \n') 126 | elif line.startswith("USB_PID"): 127 | usb_pid = line.split("=")[1].split("#")[0].strip('" \n') 128 | elif line.startswith("USB_PRODUCT"): 129 | usb_product = line.split("=")[1].split("#")[0].strip('" \n') 130 | elif line.startswith("USB_MANUFACTURER"): 131 | usb_manufacturer = line.split("=")[1].split("#")[0].strip('" \n') 132 | 133 | # CircuitPython 7 BLE-only boards 134 | elif line.startswith("CIRCUITPY_CREATOR_ID"): 135 | usb_vid = line.split("=")[1].split("#")[0].strip('" \n') 136 | elif line.startswith("CIRCUITPY_CREATION_ID"): 137 | usb_pid = line.split("=")[1].split("#")[0].strip('" \n') 138 | if usb_manufacturer == "Nadda-Reel Company LLC": 139 | continue 140 | 141 | usb_vid = normalize_vid_pid(usb_vid) 142 | usb_pid = normalize_vid_pid(usb_pid) 143 | 144 | # CircuitPython 7 BLE-only boards have no usb manuf/product 145 | description = site_path 146 | if usb_manufacturer and usb_product: 147 | description = "{0} {1}".format(usb_manufacturer, usb_product) 148 | 149 | board = { 150 | "vid": usb_vid, 151 | "pid": usb_pid, 152 | "product": usb_product, 153 | "manufacturer": usb_manufacturer, 154 | "site_path": site_path, 155 | "description": description, 156 | } 157 | boards.append(board) 158 | print( 159 | "{0}:{1} {2}, {3}".format(usb_vid, usb_pid, usb_manufacturer, usb_product) 160 | ) 161 | board_pyi_path = repo_root / "boards" / usb_vid / usb_pid 162 | board_pyi_path.mkdir(parents=True, exist_ok=True) 163 | board_pyi_file = board_pyi_path / "board.pyi" 164 | 165 | # We're going to put the common stuff from the generic board stub at the 166 | # end of the file, so we'll collect them after the loop 167 | board_stubs = {} 168 | 169 | with open(board_pyi_file, "w") as outfile: 170 | imports_string, stubs_string = parse_pins(generic_stubs, pins, board_stubs) 171 | outfile.write("from __future__ import annotations\n") 172 | outfile.write(imports_string) 173 | 174 | # start of module doc comment 175 | outfile.write('"""\n') 176 | outfile.write("board {0}\n".format(board["description"])) 177 | outfile.write( 178 | "https://circuitpython.org/boards/{0}\n".format(board["site_path"]) 179 | ) 180 | outfile.write('"""\n') 181 | 182 | # start of actual stubs 183 | outfile.write(stubs_string) 184 | 185 | for p in board_stubs: 186 | outfile.write("{0}\n".format(board_stubs[p])) 187 | return boards 188 | 189 | 190 | if __name__ == "__main__": 191 | main() 192 | -------------------------------------------------------------------------------- /scripts/install-bindings.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const globby = require('globby'); 3 | var path=require('path'); 4 | var fs = require('fs'); 5 | let re = new RegExp('../bindings-.*/node_modules/(.*)/lib/binding/(.*)/(.*).node', ''); 6 | (async () => { 7 | var filez = await globby('../bindings-*/node_modules/**/lib/binding/*/*.node'); 8 | filez.forEach(f => { 9 | console.log(`Found ${f}`); 10 | dest = f.replace(re, 'node_modules/$1/lib/binding/$2/$3.node'); 11 | parent = path.dirname(dest); 12 | console.log(`Moving ${f}`); 13 | console.log(` to ${dest}`); 14 | fs.mkdirSync(parent, {recursive: true}); 15 | fs.renameSync(f, dest); 16 | }); 17 | })(); 18 | (async () => { 19 | var filez = await globby('node_modules/**/build/Release/*.node'); 20 | filez.forEach(f => { 21 | dest = f.replace(re, 'node_modules/$1/lib/binding/$2/$3.node'); 22 | parent = path.dirname(dest);; 23 | console.log(`Deleting ${parent}`); 24 | fs.rmdirSync(parent, {recursive: true}); 25 | }); 26 | })(); -------------------------------------------------------------------------------- /src/boards/board.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem } from "vscode"; 2 | import * as fs from 'fs'; 3 | import { stringify } from "querystring"; 4 | import { normalize } from "path"; 5 | 6 | class BoardData { 7 | public vid: string; 8 | public pid: string; 9 | public product: string; 10 | public manufacturer: string; 11 | } 12 | 13 | export class Board implements QuickPickItem { 14 | public vid: string; 15 | public pid: string; 16 | public product: string; 17 | public manufacturer: string; 18 | public label: string; 19 | public description: string = ""; 20 | public site: string; 21 | 22 | public constructor(m: BoardData) { 23 | this.vid = m["vid"]; 24 | this.pid = m["pid"]; 25 | this.product = m["product"]; 26 | this.manufacturer = m["manufacturer"]; 27 | this.label = this.manufacturer + ":" + this.product; 28 | if(m["site_path"]){ 29 | this.site = `https://circuitpython.org/board/${m["site_path"]}/` 30 | }; 31 | } 32 | 33 | private static _boards: Map = null; 34 | public static loadBoards(metadataFile: string) { 35 | if (Board._boards === null) { 36 | Board._boards = new Map(); 37 | } 38 | let jsonData: Buffer = fs.readFileSync(metadataFile); 39 | let boardMetadata: Array = JSON.parse(jsonData.toString()); 40 | boardMetadata.forEach(b => { 41 | Board._boards.set(Board.key(b["vid"], b["pid"]), new Board(b)); 42 | }); 43 | } 44 | public static getBoardChoices(): Array { 45 | return Array.from(Board._boards.values()); 46 | } 47 | public static lookup(vid: string, pid: string): Board { 48 | vid = Board._normalizeHex(vid); 49 | pid = Board._normalizeHex(pid); 50 | let key: string = Board.key(vid, pid); 51 | let found: Board = Board._boards.get(key); 52 | if (found) { 53 | return found; 54 | } 55 | return new Board({ 56 | vid: vid, 57 | pid: pid, 58 | manufacturer: Board._normalizeHex(vid), 59 | product: Board._normalizeHex(pid) 60 | }); 61 | } 62 | public static key(vid: string, pid: string): string { 63 | return `${vid}:${pid}`; 64 | } 65 | private static _normalizeHex(hex: string): string { 66 | let n: string = hex; 67 | if (hex.length === 4) { 68 | n = "0x" + hex.toUpperCase(); 69 | } else if(hex.length === 6) { 70 | n = "0x" + hex.substring(2).toUpperCase(); 71 | } 72 | return n; 73 | } 74 | } -------------------------------------------------------------------------------- /src/boards/boardManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Board } from './board'; 3 | import * as path from 'path'; 4 | import { LibraryManager } from "../librarymanager/libraryManager"; 5 | import { Container } from "../container"; 6 | 7 | export class BoardManager implements vscode.Disposable { 8 | private extensionPath: string = null; 9 | public libraryPath: string = null; 10 | 11 | private _boardChoice: vscode.StatusBarItem; 12 | private _currentBoard: Board; 13 | private static _boardManager: BoardManager = null; 14 | 15 | public constructor(board: Board) { 16 | this._boardChoice = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 30); 17 | this._boardChoice.command = "circuitpython.selectBoard"; 18 | this._boardChoice.text = ""; 19 | this._boardChoice.tooltip = "Choose Circuit Python Board"; 20 | if (board !== null) { 21 | this._boardChoice.text = board.label; 22 | } 23 | this._boardChoice.show(); 24 | } 25 | 26 | public setExtensionPath(p: string) { 27 | this.extensionPath = p; 28 | let conf: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("circuitpython.board"); 29 | let vid: string = conf.get("vid"); 30 | let pid: string = conf.get("pid"); 31 | if(vid && pid) { 32 | let b: Board = Board.lookup(vid, pid); 33 | this.updateBoardChoiceStatus(b); 34 | } 35 | } 36 | 37 | public dispose() {} 38 | 39 | public async selectBoard() { 40 | const chosen = await vscode.window.showQuickPick( 41 | Board.getBoardChoices() 42 | .sort((a, b): number => { 43 | return a.label === b.label ? 0 : (a.label > b.label ? 1 : -1); 44 | }), { placeHolder: "Choose a board"}); 45 | if (chosen && chosen.label) { 46 | Container.setBoard(chosen); 47 | } 48 | } 49 | 50 | public async openBoardSite() { 51 | vscode.env.openExternal(vscode.Uri.parse(Container.getBoard().site)); 52 | } 53 | 54 | public updateBoardChoiceStatus(board: Board) { 55 | if (board) { 56 | this._boardChoice.text = board.label; 57 | } else { 58 | this._boardChoice.text = ""; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/container.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Project } from './project'; 3 | import { BoardManager } from './boards/boardManager'; 4 | import { Board } from './boards/board'; 5 | import * as path from 'path'; 6 | import { LibraryManager } from './librarymanager/libraryManager'; 7 | import { SerialMonitor } from './serialmonitor/serialMonitor'; 8 | import { DeviceManager } from './devicemanager/deviceManager'; 9 | 10 | export class Container implements vscode.Disposable { 11 | private static _instance: Container = null; 12 | 13 | public static async newInstance(context: vscode.ExtensionContext) { 14 | if(Container._instance === null) { 15 | Container._instance = new Container(context); 16 | } 17 | await Container._instance.initialize(); 18 | return Container._instance; 19 | } 20 | // TODO: Error on null 21 | public static getInstance() { 22 | return this._instance; 23 | } 24 | 25 | private _project: Project = null; 26 | private _context: vscode.ExtensionContext; 27 | private _boardManager: BoardManager; 28 | private _libraryManager: LibraryManager; 29 | private _serialMonitor: SerialMonitor; 30 | private _deviceManager: DeviceManager; 31 | 32 | 33 | public constructor(context: vscode.ExtensionContext){ 34 | this._context = context; 35 | let metadataFile: string = path.join( 36 | this._context.extensionPath, 37 | "boards", 38 | "metadata.json" 39 | ); 40 | Board.loadBoards(metadataFile); 41 | 42 | this._project = new Project(context); 43 | this._boardManager = new BoardManager(this._project.getBoard()); 44 | this._libraryManager = new LibraryManager(context.globalStoragePath); 45 | this._serialMonitor = new SerialMonitor(); 46 | 47 | /* 48 | The DeviceManager is a work in progress, and is therefore disabled by default 49 | */ 50 | //this._deviceManager = new DeviceManager(); 51 | } 52 | 53 | private async initialize() { 54 | await this._libraryManager.initialize(); 55 | this.registerCommand('library.show', () => this._libraryManager.show()); 56 | this.registerCommand('library.list', () => this._libraryManager.list()); 57 | this.registerCommand('library.update', () => this._libraryManager.update()); 58 | this.registerCommand('library.reload', () => this._libraryManager.reloadProjectLibraries()); 59 | 60 | vscode.workspace.onDidDeleteFiles((e: vscode.FileDeleteEvent) => this._libraryManager.reloadProjectLibraries()); 61 | this.registerCommand('library.fetch', () => this._libraryManager.updateBundle()); 62 | this.registerCommand('selectBoard', () => this._boardManager.selectBoard()); 63 | this.registerCommand('openBoardSite', () => this._boardManager.openBoardSite()); 64 | try { 65 | require("@serialport/bindings-cpp"); 66 | this.registerCommand('openSerialMonitor', () => this._serialMonitor.openSerialMonitor()); 67 | this.registerCommand('selectSerialPort', () => this._serialMonitor.selectSerialPort(null, null)); 68 | this.registerCommand('closeSerialMonitor', () => this._serialMonitor.closeSerialMonitor()); 69 | } catch (error) { 70 | console.log(error); 71 | } 72 | } 73 | 74 | private registerCommand(name: string, f: (...any) => any) { 75 | this._context.subscriptions.push( 76 | vscode.commands.registerCommand(`circuitpython.${name}`, f) 77 | ); 78 | } 79 | public dispose() {} 80 | 81 | // Commands 82 | public static updatePaths() { 83 | Container._instance._project.refreshAutoCompletePaths(); 84 | } 85 | public static updateBundlePath() { 86 | Container._instance._project.updateBundlePath(Container.getBundlePath()); 87 | Container.updatePaths(); 88 | } 89 | 90 | public static setBoard(board: Board) { 91 | Container._instance._project.setBoard(board); 92 | Container._instance._boardManager.updateBoardChoiceStatus(board); 93 | } 94 | 95 | public static getBoard(): Board { 96 | return Container._instance._project.getBoard(); 97 | } 98 | 99 | public static reloadProjectLibraries() { 100 | Container._instance._libraryManager.reloadProjectLibraries(); 101 | } 102 | 103 | public static getProjectLibDir(): string { 104 | return Container._instance._libraryManager.projectLibDir; 105 | } 106 | 107 | public static getMpySuffix(): string { 108 | return Container._instance._libraryManager.mpySuffix; 109 | } 110 | 111 | public static getBundlePath(): string { 112 | return Container._instance._libraryManager.bundlePath(Container.getMpySuffix()); 113 | } 114 | 115 | public static async loadBundleMetadata(): Promise { 116 | return await Container._instance._libraryManager.loadBundleMetadata(); 117 | } 118 | } -------------------------------------------------------------------------------- /src/devicemanager/deviceManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { SerialPort } from 'serialport'; 3 | import { PortInfo } from '@serialport/bindings-interface'; 4 | //import SerialPort = require("serialport"); 5 | import * as drivelist from 'drivelist'; 6 | 7 | 8 | 9 | /* 10 | There are a few scenarios in which we might manage a device 11 | 12 | 1. The workspace is the root of the devices' filesystem 13 | 14 | 2. The workspace is on disk, but there is a device attached and the user's 15 | intent is to work on disk and click something that copies everything to the 16 | device. 17 | 18 | */ 19 | export class DeviceManager implements vscode.Disposable { 20 | 21 | public static async getInstance(): Promise { 22 | if (DeviceManager._instance === null) { 23 | DeviceManager._instance = new DeviceManager(); 24 | } 25 | let d: drivelist.Drive[] = await DeviceManager._instance.findDevice(); 26 | return DeviceManager._instance; 27 | } 28 | private static _instance: DeviceManager = null; 29 | 30 | public constructor() {} 31 | 32 | private async findDevice(): Promise { 33 | let usbDevices: drivelist.Drive[] = 34 | await drivelist.list().then( 35 | (drives) => drives.filter((v,i,a) => v.isUSB) 36 | ); 37 | /* 38 | Here's fields from the Drive instance that will help 39 | drive.mountpoints.shift().label == "CIRCUITPY" 40 | drive.mountpoints.shift().path == "/Volumes/CIRCUITPY" 41 | drive.isUSB 42 | drive.size 43 | - PyRuler 66048 44 | 45 | - BackupDrive: 3000592498688 <- LOL 46 | drive.description 47 | - "Adafruit PyRuler Media" 48 | 49 | - "WD WDC WD30EFRX-68EUZN0 Media" 50 | */ 51 | let serialPorts: PortInfo[] = 52 | await SerialPort.list(); 53 | /* 54 | PortInfo 55 | 56 | port.comName - /dev/tty.usbmodem401 57 | port.manufacturer - "Adafruit Industries LLC" 58 | port.path /dev/tty.usbmodem401 59 | productId 804c 60 | vendorId: 239a 61 | */ 62 | return usbDevices; 63 | } 64 | 65 | 66 | public dispose() {} 67 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Container } from "./container"; 3 | 4 | export async function activate(context: vscode.ExtensionContext) { 5 | let pythonConfig: vscode.WorkspaceConfiguration = 6 | vscode.workspace.getConfiguration("python"); 7 | pythonConfig.update("languageServer", "Pylance"); 8 | let pythonAnalysis: Object = pythonConfig.get( 9 | "analysis.diagnosticSeverityOverrides" 10 | ); 11 | pythonAnalysis["reportMissingModuleSource"] = "none"; 12 | pythonAnalysis["reportShadowedImports"] = "none"; 13 | pythonConfig.update("analysis.diagnosticSeverityOverrides", pythonAnalysis); 14 | 15 | let container: Container = await Container.newInstance(context); 16 | } 17 | 18 | // this method is called when your extension is deactivated 19 | export function deactivate() {} 20 | -------------------------------------------------------------------------------- /src/librarymanager/library.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | // Not used yet, but may do semver checks in the future. Right now logic assumes 3 | // that the most recently downloaded bundle will be newer than the one you've 4 | // got installed if they're different. 5 | import * as semver from "semver"; 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | import * as _ from "lodash"; 9 | 10 | /* 11 | A Library can either represent an entry in the Adafruit_CircuitPython_Bundle or 12 | a library in the project. 13 | */ 14 | export class Library implements vscode.QuickPickItem { 15 | // QuickPickItem impl 16 | public label: string = null; 17 | public description: string = null; 18 | 19 | public name: string = null; 20 | public version: string = null; 21 | public repo: string = null; 22 | // Is this a compiled mpy library 23 | public mpy: boolean = false; 24 | // Is this a directory? If false, it's a single file. 25 | public isDirectory: boolean = false; 26 | // Whatever it is, it's this. 27 | public location: string = null; 28 | 29 | // Location may be a file or directory 30 | public constructor( 31 | name: string, 32 | version: string, 33 | repo: string, 34 | location: string, 35 | isDirectory: boolean 36 | ) { 37 | this.label = name; 38 | this.description = `Version: ${version}`; 39 | this.name = name; 40 | this.version = version; 41 | this.repo = repo; 42 | this.location = location; 43 | this.isDirectory = isDirectory; 44 | } 45 | 46 | /* 47 | Figures out what kind of files represent this library and routes to the 48 | appropriate function 49 | */ 50 | public static async from(p: string): Promise { 51 | let ext: string = path.extname(p); 52 | if (ext === ".py") { 53 | return Library.fromFile(p); 54 | } else if (ext === ".mpy") { 55 | return Library.fromBinaryFile(p); 56 | } else { 57 | return Library.fromDirectory(p); 58 | } 59 | } 60 | 61 | /* 62 | This handles the single source py file 63 | */ 64 | public static async fromFile(file: string): Promise { 65 | let s: fs.ReadStream = fs.createReadStream(file, { encoding: "utf8" }); 66 | 67 | return new Promise((resolve, reject) => { 68 | let name: string = path.basename(file, ".py"); 69 | let version: string = "unknown"; 70 | let repo: string = null; 71 | 72 | s.on("data", (data: string) => { 73 | let lines: string[] = data.split("\n"); 74 | lines = _.dropWhile(lines, (l) => l.startsWith("#")); 75 | while ((version === null || repo === null) && lines.length > 0) { 76 | let l: string = _.head(lines); 77 | if (l.startsWith("__version__")) { 78 | version = l.replace(/__version__\s=\s"([\d.?]+)"/, "$1").trim(); 79 | } else if (l.startsWith("__repo__")) { 80 | repo = l.replace(/__repo__\s=\s"(.+)"/, "$1"); 81 | } 82 | lines = _.tail(lines); 83 | } 84 | }).once("end", () => { 85 | resolve(new Library(name, version, repo, file, false)); 86 | }); 87 | }); 88 | } 89 | 90 | /* 91 | This handles the single binary mpy file 92 | 93 | Escape characters changed between CP6 and CP7. 94 | for 6: \xb0 95 | for 7: \x16\x16 96 | 97 | If it happens again, this page helped: https://www.w3schools.com/tags/ref_urlencode.ASP 98 | */ 99 | public static async fromBinaryFile(file: string): Promise { 100 | let s: fs.ReadStream = fs.createReadStream(file); 101 | return new Promise((resolve, reject) => { 102 | let name: string = path.basename(file, ".mpy"); 103 | let version: string = "unknown"; 104 | s.on("data", (data: string) => { 105 | let chunk: string = data.toString(); 106 | let start: number = chunk.search(/[\d*\.?]+[\x0b|\x16]+__version__/); 107 | let end: number = chunk.indexOf("__version"); 108 | version = chunk.substring(start, end).trim(); 109 | while (version.endsWith("\x16") || version.endsWith("\x0b")) { 110 | version = version.substring(0, version.length - 1); 111 | } 112 | }).once("end", () => { 113 | let l: Library = new Library(name, version, null, file, false); 114 | l.mpy = true; 115 | resolve(l); 116 | }); 117 | }); 118 | } 119 | 120 | /* 121 | This handles a directory, which makes some decisions about what to look at, 122 | then sends single files through the above functions and identifies the one 123 | with the best metadata. 124 | */ 125 | public static async fromDirectory(dir: string): Promise { 126 | let name: string = path.basename(dir); 127 | let mpy: boolean = false; 128 | let modules: Promise[] = null; 129 | let files: string[] = fs.readdirSync(dir); 130 | let deepFiles: string[] = []; 131 | files.forEach((file: string) => { 132 | deepFiles = []; 133 | if (fs.lstatSync(path.join(dir, file)).isDirectory()) { 134 | deepFiles = deepFiles.concat(fs.readdirSync(path.join(dir, file))); 135 | deepFiles = deepFiles.map((v, i, a) => path.join(file, v)); 136 | } 137 | files = files.concat(deepFiles); 138 | }); 139 | let mpyfiles: string[] = _.filter(files, (f) => path.extname(f) === ".mpy"); 140 | let pyfiles: string[] = _.filter(files, (f) => path.extname(f) === ".py"); 141 | 142 | // Check mpy first, since sometimes mpy folders have an __init__.py, but py 143 | // directories never have mpy files 144 | if (mpyfiles.length > 0) { 145 | files = mpyfiles; 146 | mpy = true; 147 | modules = files.map((v, i, a) => 148 | Library.fromBinaryFile(path.join(dir, v)) 149 | ); 150 | } else { 151 | files = pyfiles; 152 | modules = files.map((v, i, a) => Library.fromFile(path.join(dir, v))); 153 | } 154 | 155 | let potentials: Library[] = await Promise.all(modules); 156 | 157 | return new Promise((resolve, reject) => { 158 | let version: string = "unknown"; 159 | let repo: string = null; 160 | 161 | potentials = potentials.filter((v, i, a) => { 162 | return v.version !== null && v.version !== ""; 163 | }); 164 | 165 | let l: Library = _.find(potentials, (v) => v.repo !== null); 166 | if (l === undefined) { 167 | l = potentials.shift(); 168 | } 169 | version = l.version; 170 | repo = l.repo; 171 | l = new Library(name, version, repo, dir, true); 172 | l.mpy = mpy; 173 | resolve(l); 174 | }); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/librarymanager/libraryManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as axios from "axios"; 5 | import * as unzip from "unzipper"; 6 | import { String } from "typescript-string-operations"; 7 | import * as _ from "lodash"; 8 | import { Library } from "./library"; 9 | import * as globby from "globby"; 10 | import * as fs_extra from "fs-extra"; 11 | import * as trash from "trash"; 12 | import { Container } from "../container"; 13 | import { V4MAPPED } from "dns"; 14 | 15 | class LibraryQP implements vscode.QuickPickItem { 16 | // QuickPickItem impl 17 | public label: string = null; 18 | public description: string = null; 19 | 20 | public bundleLib: Library = null; 21 | public projectLib: Library = null; 22 | 23 | private op: string = null; 24 | public constructor(b: Library, p: Library) { 25 | this.bundleLib = b; 26 | this.projectLib = p; 27 | if (b === undefined) { 28 | this.op = "custom"; 29 | if (p === null) { 30 | this.label = "Custom"; 31 | this.description = "Cannot update"; 32 | } else { 33 | this.label = p.name; 34 | this.description = `v${p.version} is a custom library. Not updateable`; 35 | } 36 | } else { 37 | this.label = b.name; 38 | if (p === null) { 39 | this.op = "install"; 40 | this.description = `Install version ${b.version}`; 41 | } else if (b.version !== p.version) { 42 | this.op = "update"; 43 | this.description = `Update from v${p.version} to v${b.version}`; 44 | } else { 45 | this.op = null; 46 | this.description = `v${p.version} is installed and up to date.`; 47 | } 48 | } 49 | } 50 | 51 | public onClick() { 52 | switch (this.op) { 53 | case "install": 54 | this.install(); 55 | break; 56 | case "update": 57 | this.update(); 58 | break; 59 | 60 | default: 61 | break; 62 | } 63 | Container.reloadProjectLibraries(); 64 | } 65 | private install() { 66 | let src: string = LibraryManager.getMpy( 67 | path.basename(this.bundleLib.location) 68 | ); 69 | if (this.bundleLib.isDirectory) { 70 | fs_extra.copySync( 71 | src, 72 | path.join( 73 | Container.getProjectLibDir(), 74 | path.basename(this.bundleLib.location) 75 | ), 76 | { overwrite: true } 77 | ); 78 | } else { 79 | fs.copyFileSync( 80 | src, 81 | path.join( 82 | Container.getProjectLibDir(), 83 | path.basename(this.bundleLib.location, ".py") + ".mpy" 84 | ) 85 | ); 86 | } 87 | } 88 | 89 | private update() { 90 | this.install(); 91 | } 92 | } 93 | export class LibraryManager implements vscode.Disposable { 94 | public static BUNDLE_URL: string = 95 | "https://github.com/adafruit/Adafruit_CircuitPython_Bundle"; 96 | 97 | public static BUNDLE_SUFFIXES: string[] = ["py", "8.x-mpy", "9.x-mpy"]; 98 | public static BUNDLE_VERSION_REGEX: RegExp = /\d\d\d\d\d\d\d\d/; 99 | // storageRootDir is passed in from the extension BoardManager as 100 | // `BoardManager.globalStoragePath` We'll keep up to date libraries here, and all 101 | // instances of the extension can look here for them. 102 | private storageRootDir: string = null; 103 | 104 | // ${storageRootDir}/bundle 105 | private bundleDir: string = null; 106 | 107 | // ${storageRootDir}/bundle/${tag} 108 | private localBundleDir: string = null; 109 | 110 | // This is the current tag for the latest bundle ON DISK. 111 | public tag: string = null; 112 | 113 | // Circuit Python version running on this project's device 114 | public cpVersion = null; 115 | 116 | // mpySuffix defaults to "py", but we'll switch it on successful 117 | // identification of cpVersion 118 | public mpySuffix: string = "py"; 119 | 120 | // full path to what's effectively $workspaceRoot/lib 121 | public projectLibDir: string = null; 122 | 123 | // Metadata for Bundled libraries on disk 124 | private libraries: Map = new Map(); 125 | 126 | // Metadata for libraries in your project 127 | private workspaceLibraries: Map = new Map(); 128 | 129 | public dispose() {} 130 | 131 | public constructor(p: string) { 132 | this.setStorageRoot(p); 133 | } 134 | 135 | public async initialize() { 136 | // Get the latest Adafruit_CircuitPython_Bundle 137 | await this.updateBundle(); 138 | // Store the library metadata in memory 139 | await this.loadBundleMetadata(); 140 | 141 | // Figure out where the project is keeping libraries. 142 | this.projectLibDir = this.getProjectLibDir(); 143 | 144 | // Get their metadata 145 | console.log(this.projectLibDir); 146 | this.workspaceLibraries = await this.loadLibraryMetadata( 147 | this.projectLibDir 148 | ); 149 | 150 | this.cpVersion = this.getProjectCPVer(); 151 | if (this.cpVersion) { 152 | let v: string[] = this.cpVersion.split("."); 153 | if (LibraryManager.BUNDLE_SUFFIXES.includes(`${v[0]}.x-mpy`)) { 154 | this.mpySuffix = `${v[0]}.x-mpy`; 155 | } 156 | } 157 | } 158 | 159 | public completionPath(): string { 160 | if (this.localBundleDir === null) { 161 | // In case nothing exists yet. 162 | return null; 163 | } 164 | return this.bundlePath("py"); 165 | } 166 | 167 | public async reloadProjectLibraries() { 168 | this.workspaceLibraries = await this.loadLibraryMetadata( 169 | this.projectLibDir 170 | ); 171 | } 172 | 173 | public async show() { 174 | let choices: LibraryQP[] = this.getAllChoices(); 175 | const chosen = await vscode.window.showQuickPick(choices); 176 | if (chosen) { 177 | chosen.onClick(); 178 | } 179 | } 180 | 181 | public async list() { 182 | let choices: LibraryQP[] = this.getInstalledChoices(); 183 | const chosen = await vscode.window.showQuickPick(choices); 184 | if (chosen) { 185 | chosen.onClick(); 186 | } 187 | } 188 | 189 | public async update() { 190 | let choices: LibraryQP[] = this.getInstalledChoices(); 191 | choices.forEach((c: LibraryQP) => { 192 | c.onClick(); 193 | }); 194 | } 195 | 196 | private getAllChoices(): LibraryQP[] { 197 | let installedChoices: LibraryQP[] = this.getInstalledChoices(); 198 | let uninstalledChoices: LibraryQP[] = this.getUninstalledChoices(); 199 | return installedChoices.concat(uninstalledChoices); 200 | } 201 | 202 | private getInstalledChoices(): LibraryQP[] { 203 | let choices: LibraryQP[] = new Array(); 204 | Array.from(this.workspaceLibraries.keys()) 205 | .sort() 206 | .forEach((v, i, a) => { 207 | let b: Library = this.libraries.get(v); 208 | let p: Library = this.workspaceLibraries.get(v); 209 | choices.push(new LibraryQP(b, p)); 210 | }); 211 | return choices; 212 | } 213 | 214 | private getUninstalledChoices(): LibraryQP[] { 215 | let choices: LibraryQP[] = new Array(); 216 | Array.from(this.libraries.keys()) 217 | .sort() 218 | .forEach((v, i, a) => { 219 | let b: Library = this.libraries.get(v); 220 | if (!this.workspaceLibraries.has(v)) { 221 | choices.push(new LibraryQP(b, null)); 222 | } 223 | }); 224 | return choices; 225 | } 226 | 227 | private getProjectRoot(): string { 228 | let root: string = null; 229 | vscode.workspace.workspaceFolders.forEach((f) => { 230 | let r: string = path.join(f.uri.fsPath); 231 | if (!root && fs.existsSync(r)) { 232 | let b: string = path.join(r, "boot_out.txt"); 233 | if (fs.existsSync(b)) { 234 | root = r; 235 | } 236 | } 237 | }); 238 | if (!root) { 239 | root = vscode.workspace.workspaceFolders[0].uri.fsPath; 240 | } 241 | return root; 242 | } 243 | // Find it boot_out, so put boot_out.txt in your project root if you want this. 244 | private getProjectCPVer(): string { 245 | let confVer: string = vscode.workspace 246 | .getConfiguration("circuitpython.board") 247 | .get("version"); 248 | 249 | let bootOut: string = null; 250 | let ver: string = null; 251 | let b: string = path.join(this.getProjectRoot(), "boot_out.txt"); 252 | 253 | let exists: boolean = fs.existsSync(b); 254 | // If no boot_out.txt && configured version, use configured 255 | if (!exists && confVer) { 256 | ver = confVer; 257 | } else if (exists) { 258 | bootOut = b; 259 | try { 260 | let _a: string = fs.readFileSync(b).toString(); 261 | let _b: string[] = _a.split(";"); 262 | let _c: string = _b[0]; 263 | let _d: string[] = _c.split(" "); 264 | let _e: string = _d[2]; 265 | ver = _e; 266 | } catch (error) { 267 | ver = "unknown"; 268 | } 269 | } 270 | vscode.workspace 271 | .getConfiguration("circuitpython.board") 272 | .update("version", ver); 273 | return ver; 274 | } 275 | 276 | private getProjectLibDir(): string { 277 | let libDir: string = path.join(this.getProjectRoot(), "lib"); 278 | if (!fs.existsSync(libDir)) { 279 | fs.mkdirSync(libDir); 280 | } 281 | return libDir; 282 | } 283 | 284 | private setStorageRoot(root: string) { 285 | this.storageRootDir = root; 286 | this.bundleDir = path.join(this.storageRootDir, "bundle"); 287 | fs.mkdirSync(this.bundleDir, { recursive: true }); 288 | let tag: string = this.getMostRecentBundleOnDisk(); 289 | if (tag !== undefined && this.verifyBundle(tag)) { 290 | this.tag = tag; 291 | this.localBundleDir = path.join(this.bundleDir, tag); 292 | } 293 | } 294 | 295 | public async updateBundle() { 296 | let tag: string = await this.getLatestBundleTag(); 297 | let localBundleDir: string = path.join(this.bundleDir, tag); 298 | if (tag === this.tag) { 299 | vscode.window.showInformationMessage( 300 | `Bundle already at latest version: ${tag}` 301 | ); 302 | } else { 303 | vscode.window.showInformationMessage(`Downloading new bundle: ${tag}`); 304 | await this.getBundle(tag); 305 | this.tag = tag; 306 | this.localBundleDir = localBundleDir; 307 | vscode.window.showInformationMessage(`Bundle updated to ${tag}`); 308 | } 309 | this.verifyBundle(tag); 310 | Container.updateBundlePath(); 311 | } 312 | 313 | private verifyBundle(tag: string): boolean { 314 | let localBundleDir: string = path.join(this.bundleDir, tag); 315 | if (!fs.existsSync(localBundleDir)) { 316 | return false; 317 | } 318 | let bundles: fs.Dirent[] = fs 319 | .readdirSync(localBundleDir, { withFileTypes: true }) 320 | .sort(); 321 | 322 | let suffixRegExp: RegExp = new RegExp( 323 | `adafruit-circuitpython-bundle-(.*)-${tag}` 324 | ); 325 | 326 | let suffixes: string[] = []; 327 | 328 | bundles.forEach((b) => { 329 | /* 330 | It's possible for some operating systems to leave files in the bundle 331 | directory *cough* .DS_Store *cough*. Regardless, if there's a file in 332 | here, we can't dig deeper in its directory tree, so we'll catch them all. 333 | */ 334 | if (b.isDirectory()) { 335 | let p: string = path.join(localBundleDir, b.name); 336 | let lib: string[] = fs.readdirSync(p).filter((v, i, a) => v === "lib"); 337 | if (lib.length !== 1) { 338 | return false; 339 | } 340 | suffixes.push(b.name.match(suffixRegExp)[1]); 341 | } 342 | }); 343 | // TODO: Should not overwrite BUNDLE_SUFFIXES, better to get the suffixes 344 | // from the GitHub API 345 | 346 | //LibraryManager.BUNDLE_SUFFIXES = suffixes; 347 | this.localBundleDir = localBundleDir; 348 | 349 | // We're done. New bundle in $tag, so let's delete the ones that aren't 350 | // this. 351 | fs.readdir(this.bundleDir, { withFileTypes: true }, (err, bundles) => { 352 | bundles.forEach((b) => { 353 | if (b.isDirectory() && b.name !== this.tag) { 354 | let old: string = path.join(this.bundleDir, b.name); 355 | trash(old).then(() => null); 356 | } 357 | }); 358 | }); 359 | 360 | return true; 361 | } 362 | 363 | private getMostRecentBundleOnDisk(): string { 364 | if (!fs.existsSync(this.bundleDir)) { 365 | return null; 366 | } 367 | let tag: string = fs 368 | .readdirSync(this.bundleDir) 369 | .filter((dir: string, i: number, a: string[]) => 370 | LibraryManager.BUNDLE_VERSION_REGEX.test(dir) 371 | ) 372 | .sort() 373 | .reverse() 374 | .shift(); 375 | return tag; 376 | } 377 | /* 378 | Gets latest tag 379 | */ 380 | private async getLatestBundleTag(): Promise { 381 | let r: axios.AxiosResponse = await axios.default.get( 382 | "https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/latest", 383 | { headers: { Accept: "application/json" } } 384 | ); 385 | return await r.data.tag_name; 386 | } 387 | 388 | /* 389 | Downloads 6.x. and source bundles. Source are crucial for autocomplete 390 | */ 391 | private async getBundle(tag: string) { 392 | let metdataUrl: string = 393 | LibraryManager.BUNDLE_URL + 394 | "/releases/download/{0}/adafruit-circuitpython-bundle-{0}.json"; 395 | let urlRoot: string = 396 | LibraryManager.BUNDLE_URL + 397 | "/releases/download/{0}/adafruit-circuitpython-bundle-{1}-{0}.zip"; 398 | this.tag = tag; 399 | 400 | let metadataUrl: string = String.Format(metdataUrl, tag); 401 | fs.mkdirSync(path.join(this.storageRootDir, "bundle", tag), { 402 | recursive: true, 403 | }); 404 | 405 | for await (const suffix of LibraryManager.BUNDLE_SUFFIXES) { 406 | let url: string = String.Format(urlRoot, tag, suffix); 407 | let p: string = path.join(this.storageRootDir, "bundle", tag); 408 | 409 | await axios.default 410 | .get(url, { responseType: "stream" }) 411 | .then((response) => { 412 | response.data.pipe(unzip.Extract({ path: p })); 413 | }) 414 | .catch((error) => { 415 | console.log(`Error downloading {suffix} bundle: ${url}`); 416 | }); 417 | } 418 | 419 | let dest: string = path.join( 420 | this.storageRootDir, 421 | "bundle", 422 | tag, 423 | `adafruit-circuitpython-bundle-${tag}.json` 424 | ); 425 | 426 | await axios.default 427 | .get(metadataUrl, { responseType: "json" }) 428 | .then((response) => { 429 | fs.writeFileSync(dest, JSON.stringify(response.data), { 430 | encoding: "utf8", 431 | }); 432 | /* 433 | , (err) => { 434 | if (err) { 435 | console.log(`Error writing file: ${err}`); 436 | } else { 437 | } 438 | }); 439 | */ 440 | }) 441 | .catch((error) => { 442 | console.log(`Error downloading bundle metadata: ${metadataUrl}`); 443 | }); 444 | 445 | Container.loadBundleMetadata(); 446 | } 447 | 448 | public static getMpy(name: string): string { 449 | if (path.extname(name) === ".py" && Container.getMpySuffix() !== "py") { 450 | name = path.basename(name, ".py") + ".mpy"; 451 | } 452 | return path.join(Container.getBundlePath(), name); 453 | } 454 | 455 | public bundlePath(suffix: string): string { 456 | return path.join( 457 | this.localBundleDir, 458 | `adafruit-circuitpython-bundle-${suffix}-${this.tag}`, 459 | `lib` 460 | ); 461 | } 462 | 463 | public async loadBundleMetadata(): Promise { 464 | let bundlePath = this.bundlePath("py"); 465 | /* 466 | let bundlePath = path.join( 467 | this.localBundleDir, 468 | `adafruit-circuitpython-bundle-${this.tag}.json` 469 | ); 470 | */ 471 | this.libraries = await this.loadLibraryMetadata(bundlePath); 472 | return true; 473 | } 474 | 475 | private async loadLibraryMetadata( 476 | rootDir: string 477 | ): Promise> { 478 | let jsonMetadataFile = path.join( 479 | this.localBundleDir, 480 | `adafruit-circuitpython-bundle-${this.tag}.json` 481 | ); 482 | let rawData = fs.readFileSync(jsonMetadataFile, "utf8"); 483 | let jsonData = JSON.parse(rawData); 484 | 485 | let libDirs: string[] = await globby("*", { 486 | absolute: true, 487 | cwd: rootDir, 488 | deep: 1, 489 | onlyFiles: false, 490 | }); 491 | 492 | let libraries: Array> = libDirs.map((p, i, a) => 493 | Library.from(p).then((l) => { 494 | if (rootDir.startsWith(this.localBundleDir)) { 495 | l.version = jsonData[l.name].version; 496 | } 497 | return l; 498 | }) 499 | ); 500 | 501 | return new Promise>(async (resolve, reject) => { 502 | let libs: Array = await Promise.all(libraries).catch((error) => { 503 | return new Array(); 504 | }); 505 | 506 | let libraryMetadata: Map = new Map(); 507 | libs.forEach((l: Library) => { 508 | libraryMetadata.set(l.name, l); 509 | }); 510 | return resolve(libraryMetadata); 511 | }); 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /src/project.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Board } from './boards/board'; 3 | import * as path from 'path'; 4 | import { Container } from './container'; 5 | 6 | export class Project implements vscode.Disposable { 7 | 8 | private _context: vscode.ExtensionContext = null; 9 | private _circuitPythonVersion: string = null; 10 | 11 | private _autoCompleteBoard: string = ""; 12 | private _autoCompleteStdLib: string = ""; 13 | private _autoCompleteBundle: string = ""; 14 | private _autoCompleteExtra: string[] = []; 15 | 16 | private _boardVID: string = null; 17 | private _boardPID: string = null; 18 | private _board: Board = null; 19 | 20 | public constructor(context: vscode.ExtensionContext) { 21 | this._context = context; 22 | 23 | let autoConf: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("python.analysis"); 24 | let paths: string[] = autoConf.get("extraPaths"); 25 | 26 | // Load paths from last session 27 | if (paths.length >= 3) { 28 | this._autoCompleteBoard = paths.shift(); 29 | this._autoCompleteStdLib = paths.shift(); 30 | this._autoCompleteBundle = paths.shift(); 31 | this._autoCompleteExtra = paths; 32 | } 33 | // Overwrite stdlib stubs, since the absolute path could have been for any 34 | // system and we know for sure what we've got for them. 35 | this._autoCompleteStdLib = path.join(this._context.extensionPath, "stubs"); 36 | 37 | // Get board info from settings 38 | let boardConf: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("circuitpython.board"); 39 | 40 | // Set the CircuitPython Version, the major of which will be used for finding the right mpy files 41 | let version: string = boardConf.get("version"); 42 | if (version) { 43 | this._circuitPythonVersion = version; 44 | } 45 | 46 | let vid: string = boardConf.get("vid"); 47 | let pid: string = boardConf.get("pid"); 48 | 49 | // setBoard takes care of this._autoCompleteBoard. If vid && pid are undefined, the existing value remains. 50 | if (vid && pid) { 51 | let b: Board = Board.lookup(vid, pid); 52 | this.setBoard(b); 53 | } 54 | 55 | // At the end of init, the existing Bundle Dir is unchanged, but it'll 56 | // probably change after library manager fetches a new update. Even if it's 57 | // not a newer bundle, you may be on a different computer, and that library 58 | // manager update will fix the absolute path. 59 | } 60 | 61 | public dispose() {} 62 | 63 | public setBoard(board: Board) { 64 | if(!(this._board && 65 | this._board.vid === board.vid && 66 | this._board.pid === board.pid 67 | )) { 68 | this._boardVID = board.vid; 69 | this._boardPID = board.pid; 70 | this._board = board; 71 | this._autoCompleteBoard = path.join(this._context.extensionPath, "boards", board.vid, board.pid); 72 | this.refreshAutoCompletePaths(); 73 | vscode.workspace.getConfiguration().update("circuitpython.board.vid", board.vid); 74 | vscode.workspace.getConfiguration().update("circuitpython.board.pid", board.pid); 75 | } 76 | } 77 | 78 | public getBoard(): Board { 79 | return this._board; 80 | } 81 | 82 | public updateBundlePath(p: string) { 83 | this._autoCompleteBundle = p; 84 | this.refreshAutoCompletePaths(); 85 | } 86 | 87 | public refreshAutoCompletePaths() { 88 | let paths: string[] = [ 89 | this._autoCompleteBoard, 90 | this._autoCompleteStdLib, 91 | this._autoCompleteBundle 92 | ].concat(this._autoCompleteExtra); 93 | vscode.workspace.getConfiguration().update( 94 | "python.analysis.extraPaths", 95 | paths 96 | ); 97 | } 98 | } -------------------------------------------------------------------------------- /src/serialmonitor/serialMonitor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as os from "os"; 3 | import { SerialPort } from 'serialport'; 4 | import { Board } from "../boards/board"; 5 | import { Container } from "../container"; 6 | import { PortInfo } from '@serialport/bindings-interface'; 7 | 8 | 9 | class SerialPickItem implements vscode.QuickPickItem { 10 | public label: string; 11 | public description: string; 12 | public serialPort: PortInfo; 13 | public constructor(sp: PortInfo) { 14 | this.label = sp.path; 15 | if (sp.vendorId && sp.productId) { 16 | let b: Board = Board.lookup(sp.vendorId, sp.productId); 17 | if (b) { 18 | this.description = `${b.manufacturer}:${b.product}`; 19 | } 20 | } 21 | if(!this.description) { 22 | let bits: string[] = 23 | [ 24 | sp.manufacturer, 25 | sp.vendorId, 26 | sp.productId 27 | ].filter((v: string, i, a) => v); 28 | if(bits.length > 0) { 29 | this.description = bits.join(" | "); 30 | } 31 | } 32 | this.serialPort = sp; 33 | } 34 | } 35 | export class SerialMonitor implements vscode.Disposable { 36 | public static SERIAL_MONITOR: string = "Circuit Python Serial Monitor"; 37 | public static BAUD_RATE: number = 115200; 38 | 39 | // String representing the current port path e.g. /dev/tty.usbmodemX etc... 40 | private _currentPort: SerialPickItem; 41 | // The actual port we're working with 42 | private _serialPort: SerialPort; 43 | private _portsStatusBar: vscode.StatusBarItem; 44 | 45 | private _openPortStatusBar: vscode.StatusBarItem; 46 | private _terminal: vscode.Terminal; 47 | private _writeEmitter: vscode.EventEmitter; 48 | 49 | public constructor() { 50 | try { 51 | // SerialPort.Binding = require('@serialport/bindings'); 52 | this._writeEmitter = new vscode.EventEmitter(); 53 | let pty:vscode.Pseudoterminal = { 54 | onDidWrite: this._writeEmitter.event, 55 | open: () => this._writeEmitter.fire('Circuit Python Serial Monitor\r\n'), 56 | close: () => {}, 57 | handleInput: (data: string) => { 58 | this._serialPort.write(data); 59 | } 60 | }; 61 | this._terminal = vscode.window.createTerminal({name: SerialMonitor.SERIAL_MONITOR, pty: pty }); 62 | this._portsStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 20); 63 | this._portsStatusBar.command = "circuitpython.selectSerialPort"; 64 | this._portsStatusBar.tooltip = "Select Serial Port"; 65 | this._portsStatusBar.show(); 66 | 67 | this._openPortStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 30); 68 | this._openPortStatusBar.command = "circuitpython.openSerialMonitor"; 69 | this._openPortStatusBar.text = `$(plug)`; 70 | this._openPortStatusBar.tooltip = "Open Serial Monitor"; 71 | this._openPortStatusBar.show(); 72 | } catch (error) { 73 | console.log(error); 74 | } 75 | } 76 | 77 | public dispose() {} 78 | 79 | // vid/pid needed for usb monitoring, which we're not doing... yet? 80 | public async selectSerialPort(vid: string, pid: string) { 81 | const lists = await SerialPort.list(); 82 | // vid & pid available in SerialPort as vendorId & productId 83 | if (!lists.length) { 84 | vscode.window.showInformationMessage("No serial port is available."); 85 | return; 86 | } 87 | const chosen = await vscode.window.showQuickPick(lists.map((l: PortInfo): SerialPickItem => { 88 | return new SerialPickItem(l); 89 | }).sort((a, b): number => { 90 | return a.label === b.label ? 0 : (a.label > b.label ? 1 : -1); 91 | }), { placeHolder: "Select a serial port" }); 92 | if (chosen && chosen.label) { 93 | let b: Board = Board.lookup(chosen.serialPort.vendorId, chosen.serialPort.productId); 94 | await Container.setBoard(b); 95 | this.updatePortListStatus(chosen); 96 | } 97 | } 98 | 99 | public async openSerialMonitor() { 100 | if (!this._currentPort) { 101 | await this.selectSerialPort(null, null); 102 | if (!this._currentPort) { 103 | return; 104 | } 105 | } 106 | 107 | if (this._serialPort) { 108 | if (this._currentPort.serialPort.path !== this._serialPort.path) { 109 | await this._serialPort.close(); 110 | this._serialPort = new SerialPort({path: this._currentPort.serialPort.path, baudRate: SerialMonitor.BAUD_RATE, autoOpen: false}); 111 | } else if (this._serialPort.isOpen) { 112 | vscode.window.showWarningMessage(`Serial monitor is already opened for ${this._currentPort.serialPort.path}`); 113 | return; 114 | } 115 | } else { 116 | this._serialPort = new SerialPort({ path: this._currentPort.serialPort.path, baudRate: SerialMonitor.BAUD_RATE, autoOpen: false}); 117 | } 118 | 119 | this._terminal.show(); 120 | try { 121 | this._serialPort.open(); 122 | this.updatePortStatus(true); 123 | } catch (error) { 124 | this._writeEmitter.fire("[Error]" + error.toString() + "\r\n\r\n"); 125 | console.log( 126 | `Failed to open serial port ${this._currentPort} due to error: + ${error.toString()}`); 127 | } 128 | this._writeEmitter.fire(`[Open] Connection to ${this._currentPort.serialPort.path}${os.EOL}\r\n`); 129 | this._writeEmitter.fire(`press Ctrl-C to enter the REPL${os.EOL}\r\n`); 130 | 131 | this._serialPort.on("data", (_event) => { 132 | this._writeEmitter.fire(_event.toString()); 133 | }); 134 | this._serialPort.on("error", (_error) => { 135 | this._writeEmitter.fire("[Error]" + _error.toString() + "\r\n\r\n"); 136 | }); 137 | } 138 | 139 | // Closes the serial connection 140 | public async closeSerialMonitor() { 141 | if (this._serialPort) { 142 | const result = await this._serialPort.close(); 143 | this._serialPort = null; 144 | if (this._writeEmitter) { 145 | this._writeEmitter.fire(`[Done] Closed the serial port ${os.EOL}`); 146 | } 147 | this.updatePortStatus(false); 148 | return result; 149 | } else { 150 | return false; 151 | } 152 | } 153 | 154 | private updatePortListStatus(port: SerialPickItem) { 155 | this._currentPort = port; 156 | 157 | if (port) { 158 | this._portsStatusBar.text = port.serialPort.path; 159 | } else { 160 | this._portsStatusBar.text = "