├── .github └── workflows │ ├── docker-push.yml │ └── release.yml ├── .gitignore ├── .gitlab-ci.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── WELCOME.md ├── browser-app └── package.json ├── espressif_icon.png ├── extensions ├── base.tsconfig.json ├── esp-remote-flasher │ ├── package.json │ ├── src │ │ ├── browser │ │ │ ├── esp-remote-flasher-frontend-contribution.ts │ │ │ ├── esp-remote-flasher-frontend-module.ts │ │ │ └── remoteFlasher.ts │ │ ├── common │ │ │ ├── message.ts │ │ │ └── protocol.ts │ │ └── node │ │ │ ├── esp-remote-flasher-backend-module.ts │ │ │ └── esp-remote-flasher.ts │ └── tsconfig.json ├── esp-remote-monitor │ ├── package.json │ ├── src │ │ ├── browser │ │ │ ├── components │ │ │ │ ├── ansi-to-html.d.ts │ │ │ │ └── monitor.tsx │ │ │ ├── esp-remote-monitor-contribution.ts │ │ │ ├── esp-remote-monitor-frontend-module.ts │ │ │ ├── monitor-widget.tsx │ │ │ ├── remoteMonitor.ts │ │ │ └── style │ │ │ │ └── index.css │ │ └── common │ │ │ ├── message.ts │ │ │ └── monitor.ts │ └── tsconfig.json ├── esp-remote-welcome-page │ ├── package.json │ ├── src │ │ └── browser │ │ │ ├── esp-remote-welcome-page-contribution.ts │ │ │ └── esp-remote-welcome-page-frontend-module.ts │ └── tsconfig.json └── esp-webserial │ ├── package.json │ ├── src │ ├── browser │ │ ├── esp-webserial-frontend-contribution.ts │ │ └── esp-webserial-frontend-module.ts │ ├── common │ │ ├── message.ts │ │ └── protocol.ts │ └── node │ │ ├── esp-webserial-backend-module.ts │ │ └── esp-webserial-client.ts │ └── tsconfig.json ├── lerna.json ├── package.json └── yarn.lock /.github/workflows/docker-push.yml: -------------------------------------------------------------------------------- 1 | name: docker-push 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Set up QEMU 14 | uses: docker/setup-qemu-action@v2 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v2 18 | 19 | - name: Login to Docker Hub 20 | uses: docker/login-action@v2 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | 25 | - name: Build and push 26 | uses: docker/build-push-action@v3 27 | with: 28 | push: true 29 | tags: espbignacio/idf-web-ide:latest 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | docker: 10 | name: Build and upload release docker image 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v2 16 | 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v2 19 | 20 | - name: Login to Docker Hub 21 | uses: docker/login-action@v2 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | 26 | - name: Determine version 27 | id: version 28 | run: "echo ::set-output name=version::${GITHUB_REF:11}" 29 | 30 | - name: Build and push 31 | uses: docker/build-push-action@v3 32 | with: 33 | push: true 34 | tags: espbignacio/idf-web-ide:${{ steps.version.outputs.version }} 35 | 36 | release: 37 | name: Upload release asset 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v2 42 | with: 43 | submodules: "recursive" 44 | 45 | - name: Create Release 46 | id: create_release 47 | uses: actions/create-release@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | tag_name: ${{ github.ref }} 52 | release_name: Release ${{ github.ref }} 53 | draft: true 54 | body: | 55 | ### Release Highlights 56 | 57 | ### Features & Enhancements 58 | 59 | 60 | 61 | ### Bug Fixes 62 | 63 | 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .browser_modules 4 | lib 5 | *.log 6 | *-app/* 7 | !*-app/package.json 8 | .DS_Store 9 | vscode-extensions -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - build 4 | - deploy 5 | 6 | build_test: 7 | image: node:12-stretch 8 | stage: test 9 | tags: 10 | - test_brno 11 | script: 12 | - yarn 13 | 14 | docker_build: 15 | image: espressif/dind:1 16 | stage: build 17 | tags: ["dind", "internet", "amd64"] 18 | before_script: 19 | - echo "building docker image" 20 | script: 21 | - mkdir -p _config 22 | - echo ${DOCKER_AUTH_CONFIG} > _config/config.json 23 | - echo ${CI_DOCKER_REGISTRY} 24 | - docker build . --tag ${CI_DOCKER_REGISTRY}/idf-web-ide 25 | - docker --config=./_config push ${CI_DOCKER_REGISTRY}/idf-web-ide:latest 26 | 27 | heroku_deploy: 28 | image: docker:latest 29 | when: "manual" 30 | before_script: 31 | - echo "skipping deploying to heroku..." 32 | stage: deploy 33 | tags: 34 | - deploy 35 | script: 36 | - docker build -f Dockerfile --iidfile imageid.txt -t registry.heroku.com/idf-web . 37 | - docker login -u _ -p $HEROKU_TOKEN registry.heroku.com 38 | - docker push registry.heroku.com/idf-web 39 | - apk add --no-cache curl 40 | - echo "Docker Image ID is $(cat imageid.txt)" 41 | - |- 42 | curl -X PATCH https://api.heroku.com/apps/idf-web/formation --header "Content-Type: application/json" --header "Accept: application/vnd.heroku+json; version=3.docker-releases" --header "Authorization: Bearer ${HEROKU_TOKEN}" --data '{ "updates": [ { "type": "web", "docker_image": "'$(cat imageid.txt)'" } ] }' 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach to Docker", 9 | "type": "node", 10 | "request": "attach", 11 | "port": 9229, 12 | "address": "localhost", 13 | "localRoot": "${workspaceRoot}/browser-app/src-gen/backend/main.js", 14 | "remoteRoot": "/home/theia-idf-extension/browser-app/src-gen/backend/main.js", 15 | "sourceMaps": true 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Browser Backend", 21 | "program": "${workspaceRoot}/browser-app/src-gen/backend/main.js", 22 | "args": [ 23 | "--loglevel=info", 24 | "--port=8080", 25 | "--no-cluster", 26 | "--plugins=local-dir:vscode-extensions", 27 | "--hosted-plugin-inspect=9339" 28 | ], 29 | "env": { 30 | "NODE_ENV": "development" 31 | }, 32 | "sourceMaps": true, 33 | "outFiles": [ 34 | "${workspaceRoot}/node_modules/@theia/*/lib/**/*.js", 35 | "${workspaceRoot}/browser-app/lib/**/*.js", 36 | "${workspaceRoot}/browser-app/src-gen/**/*.js" 37 | ], 38 | "smartStep": true, 39 | "internalConsoleOptions": "openOnSessionStart", 40 | "outputCapture": "std" 41 | }, 42 | { 43 | "type": "node", 44 | "request": "launch", 45 | "name": "Start Electron Backend", 46 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 47 | "windows": { 48 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 49 | }, 50 | "program": "${workspaceRoot}/electron-app/src-gen/frontend/electron-main.js", 51 | "args": ["--loglevel=debug", "--hostname=localhost", "--no-cluster"], 52 | "env": { 53 | "NODE_ENV": "development" 54 | }, 55 | "sourceMaps": true, 56 | "outFiles": [ 57 | "${workspaceRoot}/electron-app/src-gen/frontend/electron-main.js", 58 | "${workspaceRoot}/electron-app/src-gen/backend/main.js", 59 | "${workspaceRoot}/electron-app/lib/**/*.js", 60 | "${workspaceRoot}/node_modules/@theia/*/lib/**/*.js" 61 | ], 62 | "smartStep": true, 63 | "internalConsoleOptions": "openOnSessionStart", 64 | "outputCapture": "std" 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [IDF Web IDE](https://github.com/espressif/idf-web-ide) Changelog 2 | 3 | --- 4 | 5 | All notable changes to the "Espressif IDF Web IDE" extension will be documented in this file. 6 | 7 | ## [0.0.3](https://github.com/espressif/idf-web-ide/releases/tag/v0.0.3) 8 | 9 | ### Features and enhancements 10 | 11 | - [Update ESP-IDF extension to 1.6.5 and esptool-js v0.3.1](https://github.com/espressif/idf-web-ide/pull/7) 12 | - [Update ESP-IDF extension to 1.6.2 and fix image entrypoint](https://github.com/espressif/idf-web-ide/pull/5) 13 | 14 | ## [0.0.2](https://github.com/espressif/idf-web-ide/releases/tag/v0.0.2) 15 | 16 | ### Features and enhancements 17 | 18 | - [Add WebSerial support](https://github.com/espressif/idf-web-ide/pull/2) 19 | 20 | ### Bug Fixes 21 | 22 | - [Fix Docker shield links](https://github.com/espressif/idf-web-ide/pull/4) 23 | 24 | ## [0.0.1](https://github.com/espressif/idf-web-ide/releases/tag/v0.0.1) 25 | 26 | Initial release. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM espressif/idf:v4.4.6 2 | 3 | # We use this label as a filter when cleaning our runners disk space up with docker-prune 4 | LABEL protected="true" 5 | 6 | ADD . /home/idf-web-ide/ 7 | 8 | ARG DEBIAN_FRONTEND=noninteractive 9 | RUN useradd -m iwiuser 10 | 11 | # To install dependencies 12 | RUN apt-get update \ 13 | && apt-get install -y -q ca-certificates \ 14 | && curl -sL https://deb.nodesource.com/setup_16.x | bash \ 15 | && apt install -y -q \ 16 | g++ \ 17 | git \ 18 | libglib2.0-0 \ 19 | libnuma1 \ 20 | libpixman-1-0 \ 21 | libsecret-1-dev \ 22 | make \ 23 | nodejs \ 24 | pkgconf \ 25 | && npm install -g yarn typescript \ 26 | && rm -rf /var/lib/apt/lists/* 27 | 28 | RUN cd /home/idf-web-ide/ && yarn 29 | 30 | RUN chmod g+rw /home && \ 31 | chmod g+rw /home/iwiuser && \ 32 | chmod g+rw /opt/esp && \ 33 | mkdir /home/iwiuser/projects && \ 34 | chown -R iwiuser:iwiuser /home/idf-web-ide && \ 35 | chown -R iwiuser:iwiuser /home/iwiuser/projects 36 | 37 | WORKDIR /home/idf-web-ide/browser-app/ 38 | 39 | ENV SHELL /bin/bash 40 | ENV WEB_IDE=1 41 | ENV PROJECT_DIR=/home/iwiuser/templates 42 | ENV IWI_PORT 8000 43 | ENV THEIA_WEBVIEW_EXTERNAL_ENDPOINT={{hostname}} 44 | 45 | # QEMU 46 | ENV QEMU_REL=esp-develop-20220919 47 | ENV QEMU_SHA256=f6565d3f0d1e463a63a7f81aec94cce62df662bd42fc7606de4b4418ed55f870 48 | ENV QEMU_DIST=qemu-${QEMU_REL}.tar.bz2 49 | ENV QEMU_URL=https://github.com/espressif/qemu/releases/download/${QEMU_REL}/${QEMU_DIST} 50 | 51 | ENV LC_ALL=C.UTF-8 52 | ENV LANG=C.UTF-8 53 | ENV IDF_PYTHON_ENV_PATH=/opt/esp/python_env/idf4.4_py3.8_env 54 | 55 | RUN wget --no-verbose ${QEMU_URL} \ 56 | && echo "${QEMU_SHA256} *${QEMU_DIST}" | sha256sum --check --strict - \ 57 | && tar -xf $QEMU_DIST -C /opt \ 58 | && rm ${QEMU_DIST} 59 | 60 | ENV PATH=/opt/qemu/bin:${PATH} 61 | 62 | RUN git config --global --add safe.directory /opt/esp/idf 63 | 64 | RUN echo "source /opt/esp/idf/export.sh > /dev/null 2>&1" >> ~/.bashrc 65 | 66 | RUN ${IDF_PYTHON_ENV_PATH}/bin/python -m pip install -r /home/idf-web-ide/vscode-extensions/espressif.esp-idf-extension/extension/requirements.txt 67 | RUN ${IDF_PYTHON_ENV_PATH}/bin/python -m pip install -r /home/idf-web-ide/vscode-extensions/espressif.esp-idf-extension/extension/esp_debug_adapter/requirements.txt 68 | 69 | EXPOSE ${IWI_PORT} 70 | 71 | USER iwiuser 72 | 73 | ENTRYPOINT ["/opt/esp/entrypoint.sh"] 74 | 75 | CMD yarn start:debug ${PROJECT_DIR} --hostname=0.0.0.0 --port ${IWI_PORT} 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/release/espressif/idf-web-ide.svg?style=flat-square)](https://github.com/espressif/idf-web-ide/releases/latest) 2 | ![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/espressif/idf-web-ide/docker-push.yml?label=Dockerhub%20push%20status&logo=docker) 3 | [![Docker Releases](https://img.shields.io/badge/-Docker%20Releases-blue)](https://hub.docker.com/r/espbignacio/idf-web-ide) 4 | # Espressif IDF WEB IDE 5 | 6 | Cloud or desktop IDE for [ESP-IDF](https://github.com/espressif/esp-idf) quick development with Espressif devices that includes the [ESP-IDF extension for Visual Studio Code](https://github.com/espressif/vscode-esp-idf-extension). 7 | 8 | Made with [Eclipse Theia](https://github.com/eclipse-theia/theia). 9 | 10 | ## Getting started 11 | 12 | ### Run as Docker Container (_suggested_) 13 | 14 | - Pull the latest or an existing version from [Docker Hub](https://hub.docker.com/r/espbignacio/idf-web-ide): 15 | 16 | `docker pull espbignacio/idf-web-ide` 17 | 18 | - Create and Start a container from that image, notice we are mounting host OS `${PWD}` to `/home/projects` of the container 19 | 20 | `$ docker run -d -e IWI_PORT=8080 -p 8080:8080 --platform linux/amd64 -v ${PWD}:/home/projects espbignacio/idf-web-ide` 21 | 22 | - Open `http://0.0.0.0:8080` in your browser (_use chrome for best experience_) 23 | 24 | ### Run Directly from Source 25 | 26 | - `$ git clone https://github.com/espressif/idf-web-ide.git` 27 | - `$ cd idf-web-ide` 28 | - Run `$ yarn` (_nodejs, yarn is required for this step_) 29 | - `$ cd browser-app` 30 | - `$ yarn run start --port=8080` 31 | - Open `127.0.0.1:8080` in your browser (_use chrome for best experience_) 32 | - Alternatively, build the Docker Image using the DockerFile in the source 33 | 34 | `$ docker build . --tag espbignacio/idf-web-ide --platform linux/amd64` 35 | 36 | - Run the image as container as shown in the previous section. 37 | 38 | ### Use the desktop companion to flash and monitor local Espressif devices 39 | 40 | Using the [Espressif IDF Web Desktop Companion](https://github.com/espressif/iwidc/) you can remotely flash and monitor an Espressif device from this IDF Web IDE. 41 | 42 | You can get a built executable from Windows [here](https://github.com/espressif/iwidc/releases). 43 | 44 | - `.\dist\main.exe --port PORT` with the executable to start the desktop companion and `.\dist\main.exe` to see available ports. 45 | 46 | From source code run: 47 | 48 | - `pip3 install -r ${ESP-IWIDC}/requirements.txt` to install the python requirements. 49 | - `python3 main.py` to see available serial ports. 50 | - `python3 main.py --port [SERIAL_PORT_OF_ESP_32]` to start desktop companion. 51 | -------------------------------------------------------------------------------- /WELCOME.md: -------------------------------------------------------------------------------- 1 | 2 | espressif logo 3 | 4 | 5 | # Welcome to ESP-IDF Web IDE 6 | 7 | To Flash and monitor use the `Remote` menu. You will need to get the `IDF Web IDE Desktop Companion` running with your device serial port for these commands to properly work. 8 | 9 | Other features are provided by the ESP-IDF Visual Studio Code extension. Some features might not work properly. 10 | 11 | ## Useful commands 12 | 13 | Press F1 and search for any of these commands: 14 | 15 | 1. `SDK Configuration editor` for GUI menuconfig. 16 | 2. `Build your project` 17 | 3. `Show Examples Projects` 18 | 4. `Set Espressif device target` 19 | 5. `Size analysis of the binaries` 20 | 21 | 22 | Make sure to review [ESP-IDF Visual Studio code extension](https://github.com/espressif/vscode-esp-idf-extension#table-of-content) for more information. 23 | 24 | This IDE is made with [Eclipse Theia](https://github.com/eclipse-theia). -------------------------------------------------------------------------------- /browser-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "browser-app", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "@theia/core": "latest", 7 | "@theia/editor": "latest", 8 | "@theia/filesystem": "latest", 9 | "@theia/markers": "latest", 10 | "@theia/messages": "latest", 11 | "@theia/monaco": "latest", 12 | "@theia/navigator": "latest", 13 | "@theia/plugin-ext-vscode": "latest", 14 | "@theia/preferences": "latest", 15 | "@theia/preview": "latest", 16 | "@theia/process": "latest", 17 | "@theia/terminal": "latest", 18 | "@theia/vsx-registry": "latest", 19 | "@theia/workspace": "latest", 20 | "bson": "4.0.2", 21 | "esp-remote-flasher": "0.0.1", 22 | "esp-remote-welcome-page": "0.0.1", 23 | "esp-webserial": "0.0.1", 24 | "uuid": "^3.3.3" 25 | }, 26 | "devDependencies": { 27 | "@theia/cli": "latest", 28 | "@types/bson": "4.0.0" 29 | }, 30 | "scripts": { 31 | "prepare": "yarn build && theia download:plugins --parallel=false", 32 | "build": "theia build --mode development", 33 | "start": "theia start --plugins=local-dir:../vscode-extensions", 34 | "start:debug": "theia start --log-level=debug --plugins=local-dir:../vscode-extensions", 35 | "watch": "theia build --watch --mode development" 36 | }, 37 | "theia": { 38 | "target": "browser", 39 | "frontend": { 40 | "config": { 41 | "applicationName": "ESP-IDF Theia", 42 | "preferences": { 43 | "idf.adapterTargetName": "esp32", 44 | "idf.espIdfPath": "/opt/esp/idf", 45 | "idf.customExtraPaths": "", 46 | "idf.pythonBinPath": "/opt/esp/python_env/idf4.4_py3.8_env/bin/python", 47 | "idf.toolsPath": "/opt/esp", 48 | "idf.gitPath": "/usr/bin/git", 49 | "idf.enableIdfComponentManager": true 50 | } 51 | } 52 | } 53 | }, 54 | "theiaPluginsDir": "../vscode-extensions", 55 | "theiaPlugins": { 56 | "espressif.esp-idf-extension": "https://github.com/espressif/vscode-esp-idf-extension/releases/download/v1.6.5/esp-idf-extension-open-1.6.5.vsix", 57 | "eclipse-theia.builtin-extension-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.62.3/file/eclipse-theia.builtin-extension-pack-1.62.3.vsix", 58 | "vscode-clangd": "https://open-vsx.org/api/llvm-vs-code-extensions/vscode-clangd/0.1.12/file/llvm-vs-code-extensions.vscode-clangd-0.1.12.vsix", 59 | "vscode.typescript": "https://open-vsx.org/api/vscode/typescript/1.62.3/file/vscode.typescript-1.62.3.vsix", 60 | "vscode.typescript-language-features": "https://open-vsx.org/api/vscode/typescript-language-features/1.62.3/file/vscode.typescript-language-features-1.62.3.vsix", 61 | "EditorConfig.EditorConfig": "https://open-vsx.org/api/EditorConfig/EditorConfig/0.16.6/file/EditorConfig.EditorConfig-0.16.6.vsix", 62 | "dbaeumer.vscode-eslint": "https://open-vsx.org/api/dbaeumer/vscode-eslint/2.1.1/file/dbaeumer.vscode-eslint-2.1.1.vsix" 63 | }, 64 | "theiaPluginsExcludeIds": [ 65 | "ms-vscode.js-debug-companion", 66 | "vscode.dart", 67 | "vscode.extension-editing", 68 | "vscode.ipynb", 69 | "vscode.julia", 70 | "vscode.markdown-language-features", 71 | "vscode.markdown-math", 72 | "vscode.microsoft-authentication" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /espressif_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espressif/idf-web-ide/f4f803431924d3c3d101962fb2ae1e31033225e1/espressif_icon.png -------------------------------------------------------------------------------- /extensions/base.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "noImplicitAny": true, 7 | "noEmitOnError": false, 8 | "noImplicitThis": true, 9 | "noUnusedLocals": true, 10 | "strictNullChecks": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "downlevelIteration": true, 14 | "resolveJsonModule": true, 15 | "module": "commonjs", 16 | "moduleResolution": "node", 17 | "target": "ES2017", 18 | "jsx": "react", 19 | "lib": [ 20 | "ES2017", 21 | "dom" 22 | ], 23 | "sourceMap": true, 24 | "rootDir": "src", 25 | "outDir": "lib" 26 | }, 27 | "include": [ 28 | "src" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp-remote-flasher", 3 | "keywords": [ 4 | "theia-extension" 5 | ], 6 | "version": "0.0.1", 7 | "files": [ 8 | "lib", 9 | "src" 10 | ], 11 | "dependencies": { 12 | "@theia/core": "latest" 13 | }, 14 | "devDependencies": { 15 | "rimraf": "latest", 16 | "typescript": "latest" 17 | }, 18 | "scripts": { 19 | "prepare": "yarn run clean && yarn run build", 20 | "clean": "rimraf lib", 21 | "build": "tsc", 22 | "watch": "tsc -w" 23 | }, 24 | "theiaExtensions": [ 25 | { 26 | "frontend": "lib/browser/esp-remote-flasher-frontend-module", 27 | "backend": "lib/node/esp-remote-flasher-backend-module" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/browser/esp-remote-flasher-frontend-contribution.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | CommandContribution, 4 | CommandRegistry, 5 | ILogger, 6 | MAIN_MENU_BAR, 7 | MenuContribution, 8 | MenuModelRegistry 9 | } from "@theia/core/lib/common"; 10 | import { MessageService } from "@theia/core/lib/common/message-service"; 11 | import { WorkspaceService } from "@theia/workspace/lib/browser"; 12 | import { inject, injectable } from "inversify"; 13 | import { EspFlasherBackendService } from "../common/protocol"; 14 | import { FlasherClientImpl } from "./remoteFlasher"; 15 | import { serialize } from "bson"; 16 | 17 | const EspRemoteFlasherCommand: Command = { 18 | id: "EspRemoteFlasher", 19 | label: "Remote flasher for ESP", 20 | }; 21 | 22 | @injectable() 23 | export class EspRemoteFlasherCommandContribution 24 | implements CommandContribution { 25 | constructor( 26 | @inject(EspFlasherBackendService) 27 | private readonly espFlasherBackendService: EspFlasherBackendService, 28 | @inject(ILogger) 29 | protected readonly logger: ILogger, 30 | @inject(MessageService) private readonly messageService: MessageService, 31 | @inject(WorkspaceService) 32 | protected readonly workspaceService: WorkspaceService 33 | ) {} 34 | 35 | registerCommands(registry: CommandRegistry): void { 36 | registry.registerCommand(EspRemoteFlasherCommand, { 37 | execute: async () => { 38 | if (this.workspaceService.opened) { 39 | const workspaceStat = this.workspaceService.tryGetRoots(); 40 | const flasher = FlasherClientImpl.init(); 41 | 42 | const progress = await this.messageService.showProgress( 43 | { 44 | text: "Connecting to Flasher Daemon", 45 | }, 46 | () => { 47 | flasher?.setIsFlashing(false); 48 | } 49 | ); 50 | flasher?.setIsFlashing(true); 51 | 52 | flasher?.onDidCloseConnection(() => { 53 | flasher?.setIsFlashing(false); 54 | progress.cancel(); 55 | this.messageService.error( 56 | "Lost connection with the Chip, make sure the desktop flasher tool is running with all the permissions" 57 | ); 58 | }); 59 | 60 | flasher?.onFlashDone(() => { 61 | flasher?.setIsFlashing(false); 62 | progress.cancel(); 63 | this.messageService.info("Done flashing"); 64 | }); 65 | 66 | flasher?.onFlassError(() => { 67 | flasher?.setIsFlashing(false); 68 | progress.cancel(); 69 | this.messageService.error( 70 | `Flashing failed. Check local flasher output.\n` 71 | ); 72 | }); 73 | 74 | progress.report({ 75 | message: "Getting binaries from project...", 76 | work: { done: 10, total: 100 }, 77 | }); 78 | 79 | try { 80 | const msgProtocol = await this.espFlasherBackendService.getFlashSectionsForCurrentWorkspace( 81 | workspaceStat[0].resource.toString() 82 | ); 83 | const bsonMsg = serialize(msgProtocol._message); 84 | flasher?.flash(bsonMsg); 85 | progress.report({ 86 | message: 87 | "Flashing request sent to the Chip, waiting for response", 88 | work: { done: 80, total: 100 }, 89 | }); 90 | } catch (error) { 91 | this.messageService.error(error); 92 | } 93 | } else { 94 | this.messageService.info("Open a workspace first."); 95 | } 96 | }, 97 | }); 98 | } 99 | } 100 | 101 | @injectable() 102 | export class EspRemoteFlasherMenuContribution implements MenuContribution { 103 | registerMenus(menus: MenuModelRegistry): void { 104 | const REMOTE = [...MAIN_MENU_BAR, "10_remote"]; 105 | menus.registerSubmenu(REMOTE, "Remote"); 106 | menus.registerMenuAction(REMOTE, { 107 | commandId: EspRemoteFlasherCommand.id, 108 | label: 'Remote Flash ⚡️' 109 | }); 110 | } 111 | } -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/browser/esp-remote-flasher-frontend-module.ts: -------------------------------------------------------------------------------- 1 | import { CommandContribution, MenuContribution } from "@theia/core"; 2 | import { WebSocketConnectionProvider } from "@theia/core/lib/browser"; 3 | import { ContainerModule } from "inversify"; 4 | import { 5 | FlasherClient, 6 | EspFlasherBackendService, 7 | ESP_REMOTE_FLASHER, 8 | } from "../common/protocol"; 9 | import { EspRemoteFlasherCommandContribution, EspRemoteFlasherMenuContribution } from "./esp-remote-flasher-frontend-contribution"; 10 | import { FlasherClientImpl } from "./remoteFlasher"; 11 | 12 | export default new ContainerModule((bind) => { 13 | bind(CommandContribution) 14 | .to(EspRemoteFlasherCommandContribution) 15 | .inSingletonScope(); 16 | bind(MenuContribution).to(EspRemoteFlasherMenuContribution); 17 | bind(FlasherClient) 18 | .to(FlasherClientImpl) 19 | .inSingletonScope(); 20 | 21 | bind(EspFlasherBackendService) 22 | .toDynamicValue((ctx) => { 23 | const connection = ctx.container.get(WebSocketConnectionProvider); 24 | const backendClient: FlasherClient = ctx.container.get(FlasherClient); 25 | return connection.createProxy( 26 | ESP_REMOTE_FLASHER, 27 | backendClient 28 | ); 29 | }) 30 | .inSingletonScope(); 31 | }); 32 | -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/browser/remoteFlasher.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from "@theia/core"; 2 | import { injectable } from "inversify"; 3 | import { FlasherClient, FlashEvents } from "../common/protocol"; 4 | import { deserialize } from "bson"; 5 | 6 | @injectable() 7 | export class FlasherClientImpl implements FlasherClient { 8 | constructor() { 9 | this.address = "ws://localhost:3362/flash"; 10 | FlasherClientImpl.instance = this; 11 | } 12 | private static instance: FlasherClientImpl; 13 | private clientHandler: WebSocket; 14 | private readonly address: string; 15 | 16 | private readonly onDidCloseConnectionEmitter: Emitter< 17 | FlashEvents.ConnectionClosed 18 | > = new Emitter(); 19 | readonly onDidCloseConnection = this.onDidCloseConnectionEmitter.event; 20 | 21 | private readonly onFlashErrorEmitter: Emitter< 22 | FlashEvents.FlashError 23 | > = new Emitter(); 24 | readonly onFlassError = this.onFlashErrorEmitter.event; 25 | 26 | private readonly onFlashDoneEmitter: Emitter< 27 | FlashEvents.FlashDone 28 | > = new Emitter(); 29 | readonly onFlashDone = this.onFlashDoneEmitter.event; 30 | 31 | private isRunning: boolean; 32 | 33 | public static init() { 34 | if (FlasherClientImpl.instance) { 35 | return FlasherClientImpl.instance; 36 | } 37 | } 38 | 39 | flash(data: Buffer) { 40 | if (this.clientHandler.readyState === this.clientHandler.OPEN) { 41 | this.clientHandler.send(data); 42 | } 43 | } 44 | 45 | isFlashing() { 46 | return this.isRunning; 47 | } 48 | setIsFlashing(v: boolean) { 49 | this.isRunning = v; 50 | } 51 | 52 | connect(): Promise { 53 | return new Promise((resolve, reject) => { 54 | if ( 55 | this.clientHandler && 56 | this.clientHandler.readyState == this.clientHandler.OPEN 57 | ) { 58 | return resolve("Using existing remote flasher"); 59 | } 60 | this.clientHandler = new WebSocket(this.address); 61 | 62 | this.clientHandler.onopen = () => { 63 | resolve("Remote flasher Connected"); 64 | }; 65 | 66 | this.clientHandler.onerror = (err) => { 67 | this.isRunning = false; 68 | this.onFlashErrorEmitter.fire(FlashEvents.FlashError); 69 | reject(err); 70 | }; 71 | 72 | this.clientHandler.onclose = () => { 73 | this.isRunning = false; 74 | this.onDidCloseConnectionEmitter.fire(FlashEvents.ConnectionClosed); 75 | }; 76 | this.clientHandler.onmessage = (event) => { 77 | this.messageHandler(event); 78 | }; 79 | }); 80 | } 81 | 82 | async messageHandler(event: MessageEvent) { 83 | try { 84 | console.log(event.data); 85 | let data: any = event.data; 86 | if (data && data.arrayBuffer) { 87 | data = await data.arrayBuffer(); 88 | data = Buffer.from(data); 89 | } 90 | const message = deserialize(data); 91 | if (message && message["messageType"]) { 92 | switch (message["messageType"]) { 93 | case "flash_done": 94 | return this.onFlashDoneEmitter.fire(FlashEvents.FlashDone); 95 | case "flash_error": 96 | console.error(message); 97 | return this.onFlashErrorEmitter.fire(FlashEvents.FlashError); 98 | default: 99 | return console.warn( 100 | "[Flash ⚡️]: Unrecognized message received from the chip", 101 | message 102 | ); 103 | } 104 | } else { 105 | return console.warn( 106 | "[Flash ⚡️]: Unsupported message received from the chip", 107 | message 108 | ); 109 | } 110 | } catch (error) { 111 | console.error(error); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/common/message.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export class MessageProtocol { 4 | public _message: { [key: string]: string }; 5 | 6 | constructor(messageType: string) { 7 | this._message = {}; 8 | this.addMandatoryFields(messageType); 9 | } 10 | 11 | public get message(): { [key: string]: string } { 12 | return this._message; 13 | } 14 | 15 | public add(key: string, value: any) { 16 | this._message[key] = value; 17 | } 18 | 19 | private addMandatoryFields(messageType: string) { 20 | this._message.version = "0.0.1"; 21 | this._message.messageType = messageType; 22 | this._message._uuid = this.genUUID(); 23 | } 24 | 25 | private genUUID(): string { 26 | return uuidv4(); 27 | } 28 | } -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/common/protocol.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcServer } from "@theia/core/lib/common/messaging"; 2 | import { Event } from "@theia/core/lib/common"; 3 | import { MessageProtocol } from "./message"; 4 | 5 | export const EspFlasherBackendService = Symbol("EspFlasherBackendService"); 6 | export const ESP_REMOTE_FLASHER = "/services/esp-remote-flasher"; 7 | 8 | export interface EspFlasherBackendService extends JsonRpcServer { 9 | getFlashSectionsForCurrentWorkspace( 10 | workspace: string 11 | ): Promise; 12 | } 13 | export const FlasherClient = Symbol("BackendClient"); 14 | export interface FlasherClient { 15 | connect(): Promise; 16 | flash(data: Buffer): void; 17 | onDidCloseConnection: Event; 18 | onFlashDone: Event; 19 | onFlassError: Event; 20 | setIsFlashing(v: boolean): void; 21 | } 22 | 23 | export enum FlashErrors { 24 | BuildRequiredBeforeFlash = "BUILD_REQUIRED_BEFORE_FLASH", 25 | JsonFileParseError = "JSON_FILE_PARSE_ERROR", 26 | } 27 | 28 | export enum FlashEvents { 29 | ConnectionClosed = "connection-closed", 30 | FlashDone = "flash-done", 31 | FlashError = "flash-error", 32 | } 33 | 34 | export interface FlashSectionInfo { 35 | name: string; 36 | bin: Buffer; 37 | offset: string; 38 | } 39 | -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/node/esp-remote-flasher-backend-module.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionHandler, JsonRpcConnectionHandler } from "@theia/core"; 2 | import { ContainerModule } from "inversify"; 3 | import { 4 | FlasherClient, 5 | EspFlasherBackendService, 6 | ESP_REMOTE_FLASHER, 7 | } from "../common/protocol"; 8 | import { EspFlasherBackendServiceImpl } from "./esp-remote-flasher"; 9 | 10 | export default new ContainerModule((bind) => { 11 | bind(EspFlasherBackendService) 12 | .to(EspFlasherBackendServiceImpl) 13 | .inSingletonScope(); 14 | bind(ConnectionHandler) 15 | .toDynamicValue( 16 | (ctx) => 17 | new JsonRpcConnectionHandler( 18 | ESP_REMOTE_FLASHER, 19 | (client) => { 20 | const server = ctx.container.get( 21 | EspFlasherBackendService 22 | ); 23 | server.setClient(client); 24 | client.onDidCloseConnection(() => server.dispose()); 25 | return server; 26 | } 27 | ) 28 | ) 29 | .inSingletonScope(); 30 | }); 31 | -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/src/node/esp-remote-flasher.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream, pathExists, readJSON } from "fs-extra"; 2 | import { injectable } from "inversify"; 3 | import { join, parse } from "path"; 4 | import { MessageProtocol } from "../common/message"; 5 | import { 6 | EspFlasherBackendService, 7 | FlasherClient, 8 | FlashSectionInfo, 9 | } from "../common/protocol"; 10 | 11 | @injectable() 12 | export class EspFlasherBackendServiceImpl implements EspFlasherBackendService { 13 | public client: FlasherClient; 14 | private readonly flashInfoFileName: string = "flasher_args.json"; 15 | 16 | getFlashSectionsForCurrentWorkspace( 17 | workspace: string, 18 | ): Promise { 19 | return new Promise(async (resolve, reject) => { 20 | const workspacePath = workspace.replace("file://", ""); //macosx file:// append issue with join(). 21 | const flashInfoFileName = join( 22 | workspacePath, 23 | "build", 24 | this.flashInfoFileName 25 | ); 26 | const isBuilt = await pathExists(flashInfoFileName); 27 | if (isBuilt) { 28 | try { 29 | const flashFileJson = await readJSON(flashInfoFileName); 30 | const binPromises: Promise[] = []; 31 | const results: { name: string; offset: string }[] = []; 32 | Object.keys(flashFileJson["flash_files"]).forEach((offset) => { 33 | const fileName = parse(flashFileJson["flash_files"][offset]).name; 34 | const filePath = join(workspacePath, "build", flashFileJson["flash_files"][offset]); 35 | results.push({ name: fileName, offset: filePath }); 36 | binPromises.push( 37 | this.readFileIntoBuffer(filePath, fileName, offset) 38 | ); 39 | }); 40 | const binaries = await Promise.all(binPromises); 41 | const message = new MessageProtocol("flash"); 42 | message.add("sections", binaries); 43 | const connResult = await this.client.connect(); 44 | console.log(connResult); 45 | return resolve(message); 46 | } catch (error) { 47 | this.client.setIsFlashing(false); 48 | return reject(error.message); 49 | } 50 | } else { 51 | this.client.setIsFlashing(false); 52 | return reject("Build is required before flashing"); 53 | } 54 | }); 55 | } 56 | private async readFileIntoBuffer( 57 | filePath: string, 58 | name: string, 59 | offset: string 60 | ) { 61 | return new Promise((resolve, reject) => { 62 | const fileBuffer: Buffer[] = new Array(); 63 | const stream = createReadStream(filePath); 64 | stream.on("data", (chunk: Buffer) => { 65 | fileBuffer.push(chunk); 66 | }); 67 | stream.on("end", () => { 68 | const fileBufferResult: FlashSectionInfo = { 69 | bin: Buffer.concat(fileBuffer), 70 | name, 71 | offset, 72 | }; 73 | return resolve(fileBufferResult); 74 | }); 75 | stream.on("error", (err) => { 76 | return reject(err); 77 | }); 78 | }); 79 | } 80 | 81 | dispose(): void { 82 | // do nothing 83 | } 84 | setClient(client: FlasherClient): void { 85 | this.client = client; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /extensions/esp-remote-flasher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../base.tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp-remote-monitor", 3 | "keywords": [ 4 | "theia-extension" 5 | ], 6 | "version": "0.0.1", 7 | "files": [ 8 | "lib", 9 | "src" 10 | ], 11 | "dependencies": { 12 | "ansi-to-html": "^0.6.13", 13 | "bulma": "^0.8.0", 14 | "react-html-parser": "^2.0.2", 15 | "@theia/core": "latest" 16 | }, 17 | "devDependencies": { 18 | "@types/react-html-parser": "^2.0.1", 19 | "rimraf": "latest", 20 | "typescript": "latest" 21 | }, 22 | "scripts": { 23 | "prepare": "yarn run clean && yarn run build", 24 | "clean": "rimraf lib", 25 | "build": "tsc", 26 | "watch": "tsc -w" 27 | }, 28 | "theiaExtensions": [ 29 | { 30 | "frontend": "lib/browser/esp-remote-monitor-frontend-module" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/components/ansi-to-html.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ansi-to-html'; -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/components/monitor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react/index"; 2 | import { MonitorType } from "../../common/monitor"; 3 | import { MonitorMessage } from "../monitor-widget"; 4 | import ansiToHTML = require("ansi-to-html"); 5 | import ReactHtmlParser from "react-html-parser"; 6 | 7 | export interface MonitorComponentProps { 8 | messages: Array; 9 | onNewMessage: Function; 10 | } 11 | 12 | const converter = new ansiToHTML(); 13 | 14 | export class MonitorComponent extends React.Component { 15 | messages(props: any) { 16 | const messages: Array = props.messages; 17 | const htmlMessages = messages.map((m: MonitorMessage, i: number) => { 18 | if (m.type === MonitorType.MessageFromChip) { 19 | return ( 20 |
{ReactHtmlParser(converter.toHtml(m.message))}
21 | ); 22 | } 23 | return ( 24 |
25 | {m.message} 26 |  < 27 |
28 | ); 29 | }); 30 | return
{htmlMessages}
; 31 | } 32 | 33 | handleKeyDown(e: any) { 34 | if (e.key === "Enter") { 35 | const msg = document.getElementById("message") as HTMLInputElement; 36 | this.sendMessageToChip(msg.value); 37 | msg.value = ""; 38 | } 39 | } 40 | 41 | handleClick() { 42 | const msg = document.getElementById("message") as HTMLInputElement; 43 | this.sendMessageToChip(msg.value); 44 | msg.value = ""; 45 | } 46 | 47 | sendMessageToChip(message: string) { 48 | if (!message || message === "") { 49 | return; 50 | } 51 | this.props.onNewMessage(message); 52 | this.scrollToLatestMessage(); 53 | } 54 | 55 | scrollToLatestMessage() { 56 | const messagesPane = document.getElementById("messagesPane"); 57 | if (messagesPane) { 58 | messagesPane.scrollTop = messagesPane.scrollHeight; 59 | } 60 | } 61 | 62 | render() { 63 | const htmlMessages = this.props.messages.map((m: MonitorMessage, i: number) => { 64 | if (m.type === MonitorType.MessageFromChip) { 65 | return ( 66 |
{ReactHtmlParser(converter.toHtml(m.message))}
67 | ); 68 | } 69 | return ( 70 |
71 | {m.message} 72 |  < 73 |
74 | ); 75 | }); 76 | return
77 |
78 |

79 | this.handleKeyDown(_e)} id="message" className="input fixed-height-2em" type="text" placeholder="Send message to the chip" /> 80 |

81 |

82 | this.handleClick()}>Send 83 |

84 |
85 |
86 |
{htmlMessages}
87 |
88 |
89 | } 90 | } 91 | -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/esp-remote-monitor-contribution.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "inversify"; 2 | import { MessageService, MenuModelRegistry, MAIN_MENU_BAR } from "@theia/core"; 3 | import { AbstractViewContribution } from "@theia/core/lib/browser"; 4 | import { Command, CommandRegistry } from "@theia/core/lib/common/command"; 5 | import { WorkspaceService } from "@theia/workspace/lib/browser"; 6 | import { MonitorWidget } from "./monitor-widget"; 7 | 8 | export const EspRemoteMonitorCommand: Command = { 9 | id: "EspRemoteMonitor.command", 10 | label: "Start ESP Remote Monitor", 11 | }; 12 | 13 | @injectable() 14 | export class EspRemoteMonitorWidgetContribution extends AbstractViewContribution< 15 | MonitorWidget 16 | > { 17 | constructor( 18 | @inject(WorkspaceService) 19 | private readonly workspaceService: WorkspaceService, 20 | @inject(MessageService) private readonly messageService: MessageService 21 | ) { 22 | super({ 23 | widgetId: MonitorWidget.ID, 24 | widgetName: MonitorWidget.LABEL, 25 | toggleCommandId: EspRemoteMonitorCommand.id, 26 | defaultWidgetOptions: { 27 | area: "bottom", 28 | }, 29 | }); 30 | } 31 | 32 | registerCommands(command: CommandRegistry) { 33 | command.registerCommand(EspRemoteMonitorCommand, { 34 | execute: async () => { 35 | if (!this.workspaceService.opened) { 36 | return this.messageService.error( 37 | "Open a ESP-IDF workspace folder first." 38 | ); 39 | } 40 | super.openView({ 41 | reveal: true, 42 | activate: false, 43 | }); 44 | }, 45 | }); 46 | } 47 | 48 | registerMenus(menus: MenuModelRegistry) { 49 | const REMOTE = [...MAIN_MENU_BAR, "10_remote"]; 50 | menus.registerSubmenu(REMOTE, "Remote"); 51 | menus.registerMenuAction(REMOTE, { 52 | commandId: EspRemoteMonitorCommand.id, 53 | label: "Remote Monitor 👀", 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/esp-remote-monitor-frontend-module.ts: -------------------------------------------------------------------------------- 1 | import { ContainerModule } from "inversify"; 2 | import { MonitorWidget } from "./monitor-widget"; 3 | import { 4 | bindViewContribution, 5 | FrontendApplicationContribution, 6 | WidgetFactory, 7 | } from "@theia/core/lib/browser"; 8 | import { EspRemoteMonitorWidgetContribution } from "./esp-remote-monitor-contribution"; 9 | 10 | import "../../src/browser/style/index.css"; 11 | 12 | export default new ContainerModule((bind) => { 13 | bindViewContribution(bind, EspRemoteMonitorWidgetContribution); 14 | bind(FrontendApplicationContribution).toService( 15 | EspRemoteMonitorWidgetContribution 16 | ); 17 | bind(MonitorWidget).toSelf(); 18 | bind(WidgetFactory) 19 | .toDynamicValue((ctx) => ({ 20 | id: MonitorWidget.ID, 21 | createWidget: () => ctx.container.get(MonitorWidget), 22 | })) 23 | .inSingletonScope(); 24 | }); 25 | -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/monitor-widget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react/index"; 2 | import { inject, injectable, postConstruct } from "inversify"; 3 | import { ReactWidget } from "@theia/core/lib/browser/widgets/react-widget"; 4 | import { serialize } from "bson"; 5 | import { MessageService } from "@theia/core"; 6 | import { MessageProtocol } from "../common/message"; 7 | import { MonitorType } from "../common/monitor"; 8 | import { RemoteMonitor } from "./remoteMonitor"; 9 | import { MonitorComponent } from "./components/monitor"; 10 | export interface MonitorMessage { 11 | type: MonitorType; 12 | message: any; 13 | } 14 | 15 | @injectable() 16 | export class MonitorWidget extends ReactWidget { 17 | static readonly ID = "esp.remote.flasher.widget"; 18 | static readonly LABEL = "Remote Flasher for ESP Chip"; 19 | 20 | messages: Array; 21 | 22 | @inject(MessageService) 23 | protected readonly messageService!: MessageService; 24 | 25 | @postConstruct() 26 | protected async init(): Promise { 27 | this.id = MonitorWidget.ID; 28 | this.title.label = MonitorWidget.LABEL; 29 | this.title.caption = MonitorWidget.LABEL; 30 | this.title.closable = true; 31 | this.title.iconClass = "fa fa-window-maximize"; 32 | this.messages = new Array(); 33 | this.update(); 34 | } 35 | 36 | private addNewMessage(message: MonitorMessage) { 37 | this.messages.push(message); 38 | this.update(); 39 | } 40 | 41 | private handleUserMsg(message: string) { 42 | const remoteMonitor = RemoteMonitor.init(); 43 | if (remoteMonitor.isMonitoring()) { 44 | const msgProtocol = new MessageProtocol("monitor"); 45 | msgProtocol.add("monitor-type", MonitorType.MessageToChip); 46 | msgProtocol.add("message", message); 47 | 48 | remoteMonitor.sendMessageToChip(serialize(msgProtocol.message)); 49 | 50 | this.addNewMessage({ 51 | type: MonitorType.MessageToChip, 52 | message, 53 | }); 54 | } 55 | } 56 | 57 | protected async onAfterAttach() { 58 | const remoteMonitor = RemoteMonitor.init(); 59 | remoteMonitor.onMessageFromChip((ev) => { 60 | this.addNewMessage({ 61 | type: ev.event, 62 | message: ev.data.message.toString(), 63 | }); 64 | }); 65 | remoteMonitor.onConnectionClosed(() => { 66 | this.messageService.warn( 67 | "Lost connection with the IDF Web IDE Desktop Companion App" 68 | ); 69 | this.dispose(); 70 | }); 71 | 72 | try { 73 | if (!remoteMonitor.isMonitoring()) { 74 | await remoteMonitor.start(); 75 | } 76 | } catch (error) { 77 | console.log(error); 78 | this.messageService.error("Error with IDF Web IDE Desktop Companion App"); 79 | this.dispose(); 80 | } 81 | } 82 | 83 | protected onBeforeDetach() { 84 | const remoteMonitor = RemoteMonitor.init(); 85 | remoteMonitor.stop(); 86 | } 87 | 88 | protected render() { 89 | return ( this.handleUserMsg(message)} 92 | />); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/remoteMonitor.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from "@theia/core"; 2 | import { serialize, deserialize } from "bson"; 3 | import { MessageProtocol } from "../common/message"; 4 | import { 5 | MonitorErrors, 6 | MonitorEvents, 7 | MonitorEventWithData, 8 | MonitorType, 9 | RemoteMonitorManager, 10 | } from "../common/monitor"; 11 | 12 | export class RemoteMonitor implements RemoteMonitorManager { 13 | private static instance: RemoteMonitor; 14 | private clientHandler: WebSocket; 15 | private readonly address: string; 16 | private isRunning: boolean; 17 | 18 | private readonly onConnectionClosedEmitter: Emitter< 19 | MonitorEvents.ConnectionClosed 20 | > = new Emitter(); 21 | readonly onConnectionClosed = this.onConnectionClosedEmitter.event; 22 | 23 | private readonly onMessageFromChipEmitter: Emitter< 24 | MonitorEventWithData 25 | > = new Emitter(); 26 | readonly onMessageFromChip = this.onMessageFromChipEmitter.event; 27 | 28 | public static init(): RemoteMonitor { 29 | if (!RemoteMonitor.instance) { 30 | RemoteMonitor.instance = new RemoteMonitor("ws://localhost:3362/monitor"); 31 | } 32 | return RemoteMonitor.instance; 33 | } 34 | 35 | private constructor(addr: string) { 36 | this.address = addr; 37 | } 38 | 39 | public isMonitoring(): boolean { 40 | return this.isRunning; 41 | } 42 | 43 | async start() { 44 | const messageProtocol = new MessageProtocol("monitor"); 45 | messageProtocol.add("monitor-type", MonitorType.Start); 46 | 47 | return new Promise((resolve, reject) => { 48 | if (this.isRunning) { 49 | reject(new Error(MonitorErrors.AlreadyRunning)); 50 | } 51 | this.isRunning = true; 52 | this.clientHandler = new WebSocket(this.address); 53 | this.clientHandler.onerror = (ev) => { 54 | this.isRunning = false; 55 | reject(ev); 56 | }; 57 | this.clientHandler.onclose = (ev) => { 58 | this.isRunning = false; 59 | if (ev.reason === MonitorEvents.ConnectionClosedByUser) { 60 | console.log("monitor closed by user"); 61 | return; 62 | } 63 | this.onConnectionClosedEmitter.fire(MonitorEvents.ConnectionClosed); 64 | }; 65 | 66 | this.clientHandler.onmessage = (ev) => { 67 | this.getMsgFromChipHandler(ev); 68 | }; 69 | 70 | this.clientHandler.onopen = () => { 71 | this.sendMessageToChip(serialize(messageProtocol.message)); 72 | resolve(null); 73 | }; 74 | }); 75 | } 76 | 77 | async stop() { 78 | if ( 79 | this.isRunning && 80 | this.clientHandler.readyState === this.clientHandler.OPEN 81 | ) { 82 | this.clientHandler.close(3001, MonitorEvents.ConnectionClosedByUser); 83 | } 84 | } 85 | 86 | public sendMessageToChip(msg: Buffer) { 87 | if (this.clientHandler.readyState === this.clientHandler.OPEN) { 88 | this.clientHandler.send(msg); 89 | } 90 | } 91 | 92 | private async getMsgFromChipHandler(event: MessageEvent) { 93 | try { 94 | let data: any = event.data; 95 | if (data && data.arrayBuffer) { 96 | data = await data.arrayBuffer(); 97 | } 98 | const message = deserialize(data); 99 | if (message && message["monitor-type"]) { 100 | switch (message["monitor-type"]) { 101 | case MonitorType.MessageFromChip: 102 | if (this.isRunning) { 103 | return this.onMessageFromChipEmitter.fire({ 104 | event: MonitorType.MessageFromChip, 105 | data: message, 106 | }); 107 | } 108 | this.onMessageFromChipEmitter.fire({ 109 | event: MonitorType.MessageFromChip, 110 | data: message, 111 | }); 112 | break; 113 | case MonitorType.MonitorError: 114 | return this.onMessageFromChipEmitter.fire({ 115 | event: MonitorType.MonitorError, 116 | data: message, 117 | }); 118 | default: 119 | break; 120 | } 121 | } else { 122 | return console.warn( 123 | "[Monitor 👀]: Unrecognized message received from the chip", 124 | message 125 | ); 126 | } 127 | } catch (err) { 128 | console.log("[Monitor 👀]: Failed to parse the incoming message"); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/browser/style/index.css: -------------------------------------------------------------------------------- 1 | .input { 2 | background-color: var(--theia-input-background); 3 | border-color: var(--theia-inputOption-activeBorder); 4 | color: var(--theia-input-foreground); 5 | } 6 | .input::placeholder{ 7 | color: var(--theia-input-placeholderForeground); 8 | } 9 | .input:hover { 10 | border-color: var(--theia-inputOption-activeBorder); 11 | } 12 | .input:active, 13 | .input:focus { 14 | border-color: var(--theia-inputOption-activeBorder); 15 | } 16 | 17 | .button { 18 | background-color: var(--theia-button-background); 19 | border-color: var(--theia-button-background); 20 | color: var(--theia-button-foreground); 21 | } 22 | .button:disabled { 23 | background-color: var(--theia-notifications-background); 24 | } 25 | .button:active { 26 | box-shadow: none; 27 | } 28 | .button:hover:disabled { 29 | color: var(--theia-button-foreground); 30 | } 31 | .button:hover:enabled, 32 | button > span.icon:hover, 33 | button > span.icon > i:hover { 34 | background-color: var(--theia-button-hoverBackground); 35 | border-color: var(--theia-button-background); 36 | color: var(--theia-button-foreground); 37 | } 38 | .button:focus, .button.is-focused { 39 | border-color: var(--theia-button-background); 40 | color: var(--theia-button-foreground); 41 | } 42 | 43 | .notification { 44 | color: var(--theia-foreground); 45 | background-color: var(--theia-menu-background); 46 | } 47 | 48 | 49 | .fixed-height-2em { 50 | height: 2em; 51 | } 52 | 53 | .fixed-height-100-per-minus-4em{ 54 | height: calc(100% - 4em); 55 | } 56 | 57 | .background-transparent { 58 | background-color: transparent; 59 | } 60 | 61 | .is-scrollable { 62 | overflow: auto; 63 | } -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/common/message.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export class MessageProtocol { 4 | private _message: { [key: string]: string }; 5 | 6 | constructor(messageType: string) { 7 | this._message = {}; 8 | this.addMandatoryFields(messageType); 9 | } 10 | 11 | public get message(): { [key: string]: string } { 12 | return this._message; 13 | } 14 | 15 | public add(key: string, value: any) { 16 | this._message[key] = value; 17 | } 18 | 19 | private addMandatoryFields(messageType: string) { 20 | this._message.version = "0.0.1"; 21 | this._message.messageType = messageType; 22 | this._message._uuid = this.genUUID(); 23 | } 24 | 25 | private genUUID(): string { 26 | return uuidv4(); 27 | } 28 | } -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/src/common/monitor.ts: -------------------------------------------------------------------------------- 1 | export enum MonitorType { 2 | Start = "start", 3 | Stop = "stop", 4 | MessageFromChip = "message-from-chip", 5 | MessageToChip = "message-to-chip", 6 | MonitorError = "monitor-error", 7 | } 8 | 9 | export enum MonitorEvents { 10 | ConnectionClosed = "connection-closed", 11 | ConnectionClosedByUser = "connection-closed-by-user", 12 | MonitorError = "monitor-error", 13 | MessageFromChip = "message-from-chip", 14 | MessageFromChipWithoutListener = "message-from-chip-without-listener", 15 | } 16 | 17 | export interface MonitorEventWithData { 18 | event: MonitorType; 19 | data: any; 20 | } 21 | 22 | export enum MonitorErrors { 23 | AlreadyRunning = "monitor-already-running", 24 | } 25 | 26 | export interface RemoteMonitorManager { 27 | start(): any; 28 | stop(): any; 29 | } -------------------------------------------------------------------------------- /extensions/esp-remote-monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../base.tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": [ 8 | "src" 9 | ] 10 | } -------------------------------------------------------------------------------- /extensions/esp-remote-welcome-page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp-remote-welcome-page", 3 | "keywords": [ 4 | "theia-extension" 5 | ], 6 | "version": "0.0.1", 7 | "files": [ 8 | "lib", 9 | "src" 10 | ], 11 | "devDependencies": { 12 | "rimraf": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "prepare": "yarn run clean && yarn run build", 17 | "clean": "rimraf lib", 18 | "build": "tsc", 19 | "watch": "tsc -w" 20 | }, 21 | "theiaExtensions": [ 22 | { 23 | "frontend": "lib/browser/esp-remote-welcome-page-frontend-module" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /extensions/esp-remote-welcome-page/src/browser/esp-remote-welcome-page-contribution.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject, postConstruct } from "inversify"; 2 | import { WorkspaceService } from "@theia/workspace/lib/browser"; 3 | import { PreviewUri } from "@theia/preview/lib/browser"; 4 | import { FrontendApplicationContribution, OpenerService, open } from "@theia/core/lib/browser"; 5 | import URI from "@theia/core/lib/common/uri"; 6 | const WELCOME_PAGE_STORAGE_SERVICE_KEY = "esp_idf.welcomePageDisplayed" 7 | @injectable() 8 | export class ESPWelcomePageFrontendApplicationContribution implements FrontendApplicationContribution { 9 | private storageService: Storage | undefined; 10 | constructor( 11 | @inject(WorkspaceService) private readonly workspaceService: WorkspaceService, 12 | @inject(OpenerService) private readonly openerService: OpenerService, 13 | ) { } 14 | @postConstruct() 15 | init() { 16 | this.storageService = (window && window.localStorage) ? window.localStorage : undefined; 17 | } 18 | async onDidInitializeLayout() { 19 | if (this.workspaceService.opened) { 20 | if (this.storageService?.getItem(WELCOME_PAGE_STORAGE_SERVICE_KEY) === null) { 21 | const uri1 = new URI("/home/idf-web-ide//WELCOME.md"); 22 | const uri2 = PreviewUri.encode(uri1); 23 | this.storageService?.setItem(WELCOME_PAGE_STORAGE_SERVICE_KEY, "true"); 24 | open(this.openerService, uri2, { preview: true }); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /extensions/esp-remote-welcome-page/src/browser/esp-remote-welcome-page-frontend-module.ts: -------------------------------------------------------------------------------- 1 | import { ESPWelcomePageFrontendApplicationContribution } from './esp-remote-welcome-page-contribution'; 2 | import { FrontendApplicationContribution } from "@theia/core/lib/browser"; 3 | 4 | import { ContainerModule } from "inversify"; 5 | 6 | export default new ContainerModule(bind => { 7 | bind(FrontendApplicationContribution).to(ESPWelcomePageFrontendApplicationContribution); 8 | }); -------------------------------------------------------------------------------- /extensions/esp-remote-welcome-page/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../base.tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": [ 8 | "src" 9 | ] 10 | } -------------------------------------------------------------------------------- /extensions/esp-webserial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp-webserial", 3 | "keywords": [ 4 | "theia-extension" 5 | ], 6 | "version": "0.0.1", 7 | "files": [ 8 | "lib", 9 | "src" 10 | ], 11 | "dependencies": { 12 | "@theia/core": "latest", 13 | "esptool-js": "^0.3.1", 14 | "crypto-js": "^4.1.1" 15 | }, 16 | "devDependencies": { 17 | "@types/w3c-web-serial": "^1.0.3", 18 | "@types/crypto-js": "^4.1.1", 19 | "rimraf": "latest", 20 | "typescript": "latest" 21 | }, 22 | "scripts": { 23 | "prepare": "yarn run clean && yarn run build", 24 | "clean": "rimraf lib", 25 | "build": "tsc", 26 | "watch": "tsc -w" 27 | }, 28 | "theiaExtensions": [ 29 | { 30 | "frontend": "lib/browser/esp-webserial-frontend-module", 31 | "backend": "lib/node/esp-webserial-backend-module" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /extensions/esp-webserial/src/browser/esp-webserial-frontend-contribution.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | CommandContribution, 4 | CommandRegistry, 5 | ILogger, 6 | MAIN_MENU_BAR, 7 | MenuContribution, 8 | MenuModelRegistry, 9 | QuickInputService, 10 | } from "@theia/core/lib/common"; 11 | import { MessageService } from "@theia/core/lib/common/message-service"; 12 | import { WorkspaceService } from "@theia/workspace/lib/browser"; 13 | import { inject, injectable } from "inversify"; 14 | import { 15 | IEspLoaderTerminal, 16 | ESPLoader, 17 | FlashOptions, 18 | Transport, 19 | LoaderOptions, 20 | } from "esptool-js"; 21 | import { OutputChannelManager } from "@theia/output/lib/browser/output-channel"; 22 | import { TerminalWidget } from "@theia/terminal/lib/browser/base/terminal-widget"; 23 | import { TerminalService } from "@theia/terminal/lib/browser/base/terminal-service"; 24 | import { EspWebSerialBackendService, PartitionInfo } from "../common/protocol"; 25 | import { enc, MD5 } from "crypto-js"; 26 | 27 | const EspWebSerialDisconnectCommand: Command = { 28 | id: "EspWebSerial", 29 | label: "Disconnect device to Esptool", 30 | }; 31 | 32 | const EspWebSerialFlashCommand: Command = { 33 | id: "EspWebSerialFlash", 34 | label: "Flash with WebSerial ⚡️", 35 | }; 36 | 37 | const EspWebSerialMonitorCommand: Command = { 38 | id: "EspWebSerialMonitor", 39 | label: "Monitor with WebSerial ⚡️", 40 | }; 41 | 42 | @injectable() 43 | export class EspWebSerialCommandContribution implements CommandContribution { 44 | constructor( 45 | @inject(EspWebSerialBackendService) 46 | protected readonly espWebSerialBackendService: EspWebSerialBackendService, 47 | @inject(ILogger) protected readonly logger: ILogger, 48 | @inject(MessageService) private readonly messageService: MessageService, 49 | @inject(QuickInputService) 50 | private readonly quickInputService: QuickInputService, 51 | @inject(WorkspaceService) 52 | protected readonly workspaceService: WorkspaceService, 53 | @inject(OutputChannelManager) 54 | protected readonly outputChannelManager: OutputChannelManager, 55 | @inject(TerminalService) 56 | protected readonly terminalService: TerminalService 57 | ) {} 58 | 59 | chip: string; 60 | port: SerialPort | undefined; 61 | connected = false; 62 | isConsoleClosed = false; 63 | transport: Transport | undefined; 64 | esploader: ESPLoader; 65 | terminal: TerminalWidget; 66 | 67 | registerCommands(registry: CommandRegistry): void { 68 | registry.registerCommand(EspWebSerialDisconnectCommand, { 69 | execute: async () => { 70 | if (this.transport) { 71 | await this.transport.disconnect(); 72 | await this.transport.waitForUnlock(1000); 73 | this.transport = undefined; 74 | } 75 | if (this.port) { 76 | await this.port?.close(); 77 | this.port = undefined; 78 | } 79 | if (this.terminal) { 80 | this.terminal.dispose(); 81 | } 82 | }, 83 | }); 84 | 85 | registry.registerCommand(EspWebSerialFlashCommand, { 86 | execute: async () => { 87 | if (this.workspaceService.opened) { 88 | if (!navigator.serial) { 89 | return undefined; 90 | } 91 | this.port = await navigator.serial.requestPort(); 92 | if (!this.port) { 93 | return undefined; 94 | } 95 | this.transport = new Transport(this.port); 96 | 97 | const workspaceStat = this.workspaceService.tryGetRoots(); 98 | const progress = await this.messageService.showProgress({ 99 | text: "Flashing with WebSerial...", 100 | }); 101 | progress.report({ 102 | message: "Getting binaries from project...", 103 | work: { done: 10, total: 100 }, 104 | }); 105 | try { 106 | const items = [ 107 | { label: "921600" }, 108 | { label: "460800" }, 109 | { label: "230400" }, 110 | { label: "115200" }, 111 | ]; 112 | const selectedBaudRate = 113 | await this.quickInputService?.showQuickPick(items, { 114 | placeholder: "Choose connection baudrate", 115 | }); 116 | const baudRate = selectedBaudRate 117 | ? parseInt(selectedBaudRate.label) 118 | : 921600; 119 | const outputChnl = 120 | this.outputChannelManager.getChannel("WebSerial Flash"); 121 | outputChnl.show({ preserveFocus: true }); 122 | const clean = () => { 123 | outputChnl.clear(); 124 | }; 125 | const writeLine = (data: string) => { 126 | outputChnl.appendLine(data); 127 | }; 128 | const write = (data: string) => { 129 | outputChnl.append(data); 130 | }; 131 | 132 | const loaderTerminal: IEspLoaderTerminal = { 133 | clean, 134 | write, 135 | writeLine, 136 | }; 137 | const loaderOptions = { 138 | transport: this.transport, 139 | baudrate: baudRate, 140 | terminal: loaderTerminal, 141 | } as LoaderOptions; 142 | this.esploader = new ESPLoader(loaderOptions); 143 | this.connected = true; 144 | this.chip = await this.esploader.main_fn(); 145 | const msgProtocol = 146 | await this.espWebSerialBackendService.getFlashSectionsForCurrentWorkspace( 147 | workspaceStat[0].resource.toString() 148 | ); 149 | const fileArray = msgProtocol._message.sections as PartitionInfo[]; 150 | fileArray.forEach((element) => { 151 | outputChnl.appendLine( 152 | `File ${element.name} Address: ${element.address} HASH: ${MD5( 153 | enc.Latin1.parse(element.data) 154 | ).toString()}` 155 | ); 156 | }); 157 | const flashSize = msgProtocol._message.flash_size; 158 | const flashMode = msgProtocol._message.flash_mode; 159 | const flashFreq = msgProtocol._message.flash_freq; 160 | 161 | progress.report({ 162 | message: `Flashing device (size: ${flashSize} mode: ${flashMode} frequency: ${flashFreq})...`, 163 | }); 164 | const flashOptions = { 165 | fileArray, 166 | flashSize: "keep", 167 | flashMode: "keep", 168 | flashFreq: "keep", 169 | eraseAll: false, 170 | compress: true, 171 | reportProgress: ( 172 | fileIndex: number, 173 | written: number, 174 | total: number 175 | ) => { 176 | progress.report({ 177 | message: `${fileArray[fileIndex].name}.bin: (${written}/${total})`, 178 | }); 179 | outputChnl.appendLine(`Image ${fileArray[fileIndex].name}.bin: (${written}/${total})`); 180 | }, 181 | calculateMD5Hash: (image: string) => 182 | MD5(enc.Latin1.parse(image)).toString(), 183 | } as FlashOptions; 184 | await this.esploader.write_flash(flashOptions); 185 | progress.cancel(); 186 | this.messageService.info("Done flashing"); 187 | await this.transport.disconnect(); 188 | await this.transport.waitForUnlock(1000); 189 | this.transport = undefined; 190 | this.port = undefined; 191 | } catch (error) { 192 | progress.cancel(); 193 | const errMsg = 194 | error && error.message 195 | ? error.message 196 | : typeof error === "string" 197 | ? error 198 | : "Something went wrong"; 199 | console.log(error); 200 | this.messageService.error(errMsg); 201 | } 202 | } else { 203 | this.messageService.info("Open a workspace first."); 204 | } 205 | }, 206 | }); 207 | 208 | registry.registerCommand(EspWebSerialMonitorCommand, { 209 | execute: async () => { 210 | if (this.transport === undefined) { 211 | if (!navigator.serial) { 212 | return undefined; 213 | } 214 | this.port = await navigator.serial.requestPort(); 215 | if (!this.port) { 216 | return undefined; 217 | } 218 | this.transport = new Transport(this.port); 219 | await this.transport.connect(); 220 | } 221 | 222 | try { 223 | this.terminal = await this.terminalService.newTerminal({ 224 | id: "webserial-monitor", 225 | title: "Monitor with WebSerial", 226 | }); 227 | await this.terminal.start(); 228 | this.terminalService.open(this.terminal); 229 | this.isConsoleClosed = false; 230 | this.terminal.onDidDispose(async () => { 231 | this.isConsoleClosed = true; 232 | await this.transport?.disconnect(); 233 | this.transport = undefined; 234 | this.port = undefined; 235 | }); 236 | 237 | this.terminal.onKey(async (keyEvent) => { 238 | console.log( 239 | `terminal 'onKey' event: { key: '${keyEvent.key}', code: ${keyEvent.domEvent.code} }` 240 | ); 241 | if ( 242 | keyEvent.domEvent.code === "KeyC" || 243 | keyEvent.domEvent.code === "BracketRight" 244 | ) { 245 | this.terminal.dispose(); 246 | } 247 | }); 248 | 249 | while (true && !this.isConsoleClosed) { 250 | let val = await this.transport.rawRead(); 251 | if (typeof val !== "undefined") { 252 | let valStr = Buffer.from(val.buffer).toString(); 253 | this.terminal.write(valStr); 254 | } else { 255 | break; 256 | } 257 | } 258 | await this.transport.waitForUnlock(1500); 259 | } catch (error) { 260 | const err = error && error.message ? error.message : error; 261 | console.log(error); 262 | this.messageService.error(err); 263 | } 264 | }, 265 | }); 266 | } 267 | } 268 | 269 | @injectable() 270 | export class EspWebSerialMenuContribution implements MenuContribution { 271 | registerMenus(menus: MenuModelRegistry): void { 272 | const REMOTE = [...MAIN_MENU_BAR, "10_remote"]; 273 | menus.registerSubmenu(REMOTE, "Remote"); 274 | menus.registerMenuAction(REMOTE, { 275 | commandId: EspWebSerialDisconnectCommand.id, 276 | label: EspWebSerialDisconnectCommand.label, 277 | }); 278 | menus.registerMenuAction(REMOTE, { 279 | commandId: EspWebSerialFlashCommand.id, 280 | label: EspWebSerialFlashCommand.label, 281 | }); 282 | menus.registerMenuAction(REMOTE, { 283 | commandId: EspWebSerialMonitorCommand.id, 284 | label: EspWebSerialMonitorCommand.label, 285 | }); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /extensions/esp-webserial/src/browser/esp-webserial-frontend-module.ts: -------------------------------------------------------------------------------- 1 | import { CommandContribution, MenuContribution } from "@theia/core"; 2 | import { ContainerModule } from "inversify"; 3 | import { WebSocketConnectionProvider } from "@theia/core/lib/browser"; 4 | import { 5 | EspWebSerialCommandContribution, 6 | EspWebSerialMenuContribution, 7 | } from "./esp-webserial-frontend-contribution"; 8 | import { 9 | EspWebSerialBackendService, 10 | ESP_WEBSERIAL_FLASHER, 11 | } from "../common/protocol"; 12 | 13 | export default new ContainerModule((bind) => { 14 | bind(CommandContribution) 15 | .to(EspWebSerialCommandContribution) 16 | .inSingletonScope(); 17 | bind(MenuContribution).to(EspWebSerialMenuContribution); 18 | 19 | bind(EspWebSerialBackendService) 20 | .toDynamicValue((ctx) => { 21 | const connection = ctx.container.get(WebSocketConnectionProvider); 22 | return connection.createProxy( 23 | ESP_WEBSERIAL_FLASHER 24 | ); 25 | }) 26 | .inSingletonScope(); 27 | }); 28 | -------------------------------------------------------------------------------- /extensions/esp-webserial/src/common/message.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export class MessageProtocol { 4 | public _message: { [key: string]: any }; 5 | 6 | constructor(messageType: string) { 7 | this._message = {}; 8 | this.addMandatoryFields(messageType); 9 | } 10 | 11 | public get message(): { [key: string]: string } { 12 | return this._message; 13 | } 14 | 15 | public add(key: string, value: any) { 16 | this._message[key] = value; 17 | } 18 | 19 | private addMandatoryFields(messageType: string) { 20 | this._message.version = "0.0.1"; 21 | this._message.messageType = messageType; 22 | this._message._uuid = this.genUUID(); 23 | } 24 | 25 | private genUUID(): string { 26 | return uuidv4(); 27 | } 28 | } -------------------------------------------------------------------------------- /extensions/esp-webserial/src/common/protocol.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "@theia/core/lib/common"; 2 | import { MessageProtocol } from "./message"; 3 | 4 | export const EspWebSerialBackendService = Symbol("EspWebSerialBackendService"); 5 | export const ESP_WEBSERIAL_FLASHER = "/services/esp-webserial"; 6 | 7 | export interface EspWebSerialBackendService { 8 | getFlashSectionsForCurrentWorkspace( 9 | workspace: string 10 | ): Promise; 11 | } 12 | 13 | export const WebSerialFlasherClient = Symbol("WebSerialBackendClient"); 14 | 15 | export interface WebSerialClient { 16 | connect(): Promise; 17 | flash(data: Buffer): void; 18 | onDidCloseConnection: Event; 19 | onFlashDone: Event; 20 | onFlassError: Event; 21 | setIsFlashing(v: boolean): void; 22 | } 23 | 24 | export enum FlashErrors { 25 | BuildRequiredBeforeFlash = "BUILD_REQUIRED_BEFORE_FLASH", 26 | JsonFileParseError = "JSON_FILE_PARSE_ERROR", 27 | } 28 | 29 | export enum FlashEvents { 30 | ConnectionClosed = "connection-closed", 31 | FlashDone = "flash-done", 32 | FlashError = "flash-error", 33 | } 34 | 35 | export interface PartitionInfo { 36 | name: string; 37 | data: string; 38 | address: number; 39 | } 40 | -------------------------------------------------------------------------------- /extensions/esp-webserial/src/node/esp-webserial-backend-module.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionHandler, JsonRpcConnectionHandler } from "@theia/core"; 2 | import { ContainerModule } from "inversify"; 3 | import { 4 | EspWebSerialBackendService, 5 | ESP_WEBSERIAL_FLASHER, 6 | } from "../common/protocol"; 7 | import { EspWebSerialBackendServiceImpl } from "./esp-webserial-client"; 8 | 9 | export default new ContainerModule((bind) => { 10 | bind(EspWebSerialBackendService) 11 | .to(EspWebSerialBackendServiceImpl) 12 | .inSingletonScope(); 13 | bind(ConnectionHandler) 14 | .toDynamicValue( 15 | (ctx) => 16 | new JsonRpcConnectionHandler(ESP_WEBSERIAL_FLASHER, () => { 17 | return ctx.container.get( 18 | EspWebSerialBackendService 19 | ); 20 | }) 21 | ) 22 | .inSingletonScope(); 23 | }); 24 | -------------------------------------------------------------------------------- /extensions/esp-webserial/src/node/esp-webserial-client.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { pathExists, readJSON, readFile } from "fs-extra"; 3 | import { join, parse } from "path"; 4 | import { EspWebSerialBackendService, PartitionInfo } from "../common/protocol"; 5 | import { MessageProtocol } from "../common/message"; 6 | 7 | @injectable() 8 | export class EspWebSerialBackendServiceImpl 9 | implements EspWebSerialBackendService 10 | { 11 | private readonly flashInfoFileName: string = "flasher_args.json"; 12 | 13 | getFlashSectionsForCurrentWorkspace(workspace: string) { 14 | return new Promise(async (resolve, reject) => { 15 | const workspacePath = workspace.replace("file://", ""); 16 | const flashInfoFileName = join( 17 | workspacePath, 18 | "build", 19 | this.flashInfoFileName 20 | ); 21 | const isBuilt = await pathExists(flashInfoFileName); 22 | if (!isBuilt) { 23 | return reject("Build before flashing"); 24 | } 25 | const flashFileJson = await readJSON(flashInfoFileName); 26 | const binPromises: Promise[] = []; 27 | Object.keys(flashFileJson["flash_files"]).forEach((offset) => { 28 | const fileName = parse(flashFileJson["flash_files"][offset]).name; 29 | const filePath = join( 30 | workspacePath, 31 | "build", 32 | flashFileJson["flash_files"][offset] 33 | ); 34 | binPromises.push(this.readFileIntoBuffer(filePath, fileName, offset)); 35 | }); 36 | const binaries = await Promise.all(binPromises); 37 | const message = new MessageProtocol("flash"); 38 | message.add("sections", binaries); 39 | message.add("flash_size", flashFileJson["flash_settings"]["flash_size"]); 40 | message.add("flash_mode", flashFileJson["flash_settings"]["flash_mode"]); 41 | message.add("flash_freq", flashFileJson["flash_settings"]["flash_freq"]); 42 | return resolve(message); 43 | }); 44 | } 45 | 46 | private async readFileIntoBuffer( 47 | filePath: string, 48 | name: string, 49 | offset: string 50 | ) { 51 | const fileData = await readFile(filePath, "binary"); 52 | return { 53 | data: fileData, 54 | name, 55 | address: parseInt(offset), 56 | } as PartitionInfo; 57 | } 58 | 59 | dispose(): void {} 60 | } 61 | -------------------------------------------------------------------------------- /extensions/esp-webserial/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../base.tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "rootDir": "src", 6 | "outDir": "lib" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.4.0", 3 | "version": "0.0.0", 4 | "useWorkspaces": true, 5 | "npmClient": "yarn", 6 | "command": { 7 | "run": { 8 | "stream": true 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "prepare": "lerna run prepare", 5 | "rebuild:browser": "theia rebuild:browser", 6 | "rebuild:electron": "theia rebuild:electron", 7 | "start:browser": "yarn rebuild:browser && yarn --cwd browser-app start:debug --hostname=127.0.0.1 --port=8080", 8 | "start:electron": "yarn rebuild:electron && yarn --cwd electron-app start", 9 | "watch": "lerna run --parallel watch" 10 | }, 11 | "devDependencies": { 12 | "lerna": "2.4.0" 13 | }, 14 | "workspaces": [ 15 | "extensions/*", 16 | "browser-app" 17 | ], 18 | "version": "0.0.2" 19 | } 20 | --------------------------------------------------------------------------------