├── .eslintrc.js ├── .github └── workflows │ └── build_and_deploy.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── images │ ├── icons-1024.png │ ├── icons-192.png │ └── icons-512.png ├── src └── index.ts ├── tsconfig.json └── vite.config.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | "extends": [ 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/eslint-recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "google" 23 | ], 24 | "rules": { 25 | "@typescript-eslint/no-explicit-any": "off", 26 | }, 27 | "env": { 28 | "browser": true, 29 | "es6": true 30 | }, 31 | "parser": "@typescript-eslint/parser", 32 | "plugins": ["@typescript-eslint"], 33 | } -------------------------------------------------------------------------------- /.github/workflows/build_and_deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | # Build job 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | - name: Install NPM packages 26 | run: npm ci 27 | - name: Build 28 | run: npm run build 29 | env: 30 | CI: true 31 | - name: Upload artifacts 32 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 33 | uses: actions/upload-pages-artifact@v3 34 | with: 35 | path: dist/ 36 | 37 | # Deploy job 38 | deploy: 39 | # Add a dependency to the build job 40 | needs: build 41 | 42 | if: ${{ github.ref == 'refs/heads/main' }} 43 | 44 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 45 | permissions: 46 | pages: write # to deploy to Pages 47 | id-token: write # to verify the deployment originates from an appropriate source 48 | 49 | # Deploy to the github-pages environment 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | 54 | # Specify runner + deployment step 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Deploy to GitHub Pages 58 | id: deployment 59 | uses: actions/deploy-pages@v4 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /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 2014 Google Inc 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. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serial Terminal 2 | 3 | This repository contains a Progressive Web App that allows the user to 4 | communicate with a locally connected serial device through an interactive 5 | terminal. This provides a demonstration of the [Serial 6 | API](https://wicg.github.io/serial/). 7 | 8 | This API is available starting with Chrome 89, Edge 89, and Opera 76. 9 | 10 | ## Privacy 11 | 12 | This application is served statically and is cached for offline use. No 13 | analytics are collected. All communication with the serial device happens 14 | locally. 15 | 16 | ## Building 17 | 18 | This project is written in TypeScript and uses npm and Vite to manage 19 | dependencies and automate the build process. To get started clone the 20 | repository and install dependencies by running, 21 | 22 | ```sh 23 | npm install 24 | ``` 25 | 26 | To create a production build in the `dist` folder run, 27 | 28 | ```sh 29 | npm run build 30 | ``` 31 | 32 | To start a local development server run, 33 | 34 | ```sh 35 | npm run dev 36 | ``` 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 28 | 29 | 30 |
31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |
45 | 46 | 47 | 60 | 61 | 62 | 66 | 67 | 72 | 73 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 94 |
95 |
96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serial-terminal", 3 | "version": "0.0.1", 4 | "description": "A demonstration of the Serial API providing an interactive serial console.", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/GoogleChromeLabs/serial-terminal.git" 9 | }, 10 | "dependencies": { 11 | "@types/w3c-web-serial": "^1.0.3", 12 | "web-serial-polyfill": "^1.0.14", 13 | "xterm": "^5.2.1", 14 | "xterm-addon-fit": "^0.7.0", 15 | "xterm-addon-web-links": "^0.8.0" 16 | }, 17 | "devDependencies": { 18 | "@types/web": "0.0.71", 19 | "@typescript-eslint/eslint-plugin": "^5.32.0", 20 | "@typescript-eslint/parser": "^5.32.0", 21 | "eslint": "^8.9.0", 22 | "eslint-config-google": "^0.14.0", 23 | "typescript": "^4.7.4", 24 | "vite": "^6.3.4", 25 | "vite-plugin-eslint": "^1.8.1", 26 | "vite-plugin-pwa": "^0.21.1" 27 | }, 28 | "scripts": { 29 | "dev": "vite", 30 | "build": "tsc && vite build", 31 | "preview": "vite preview" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/images/icons-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/serial-terminal/2af0de6feb2e285ceba0afe5e70c8a740f89e66e/public/images/icons-1024.png -------------------------------------------------------------------------------- /public/images/icons-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/serial-terminal/2af0de6feb2e285ceba0afe5e70c8a740f89e66e/public/images/icons-192.png -------------------------------------------------------------------------------- /public/images/icons-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/serial-terminal/2af0de6feb2e285ceba0afe5e70c8a740f89e66e/public/images/icons-512.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Terminal} from 'xterm'; 18 | import {FitAddon} from 'xterm-addon-fit'; 19 | import {WebLinksAddon} from 'xterm-addon-web-links'; 20 | import 'xterm/css/xterm.css'; 21 | import { 22 | serial as polyfill, SerialPort as SerialPortPolyfill, 23 | } from 'web-serial-polyfill'; 24 | 25 | /** 26 | * Elements of the port selection dropdown extend HTMLOptionElement so that 27 | * they can reference the SerialPort they represent. 28 | */ 29 | declare class PortOption extends HTMLOptionElement { 30 | port: SerialPort | SerialPortPolyfill; 31 | } 32 | 33 | let portSelector: HTMLSelectElement; 34 | let connectButton: HTMLButtonElement; 35 | let baudRateSelector: HTMLSelectElement; 36 | let customBaudRateInput: HTMLInputElement; 37 | let dataBitsSelector: HTMLSelectElement; 38 | let paritySelector: HTMLSelectElement; 39 | let stopBitsSelector: HTMLSelectElement; 40 | let flowControlCheckbox: HTMLInputElement; 41 | let echoCheckbox: HTMLInputElement; 42 | let flushOnEnterCheckbox: HTMLInputElement; 43 | let autoconnectCheckbox: HTMLInputElement; 44 | 45 | let portCounter = 1; 46 | let port: SerialPort | SerialPortPolyfill | undefined; 47 | let reader: ReadableStreamDefaultReader | ReadableStreamBYOBReader | undefined; 48 | 49 | const urlParams = new URLSearchParams(window.location.search); 50 | const usePolyfill = urlParams.has('polyfill'); 51 | const bufferSize = 8 * 1024; // 8kB 52 | 53 | const term = new Terminal({ 54 | scrollback: 10_000, 55 | }); 56 | 57 | const fitAddon = new FitAddon(); 58 | term.loadAddon(fitAddon); 59 | 60 | term.loadAddon(new WebLinksAddon()); 61 | 62 | const encoder = new TextEncoder(); 63 | let toFlush = ''; 64 | term.onData((data) => { 65 | if (echoCheckbox.checked) { 66 | term.write(data); 67 | } 68 | 69 | if (port?.writable == null) { 70 | console.warn(`unable to find writable port`); 71 | return; 72 | } 73 | 74 | const writer = port.writable.getWriter(); 75 | 76 | if (flushOnEnterCheckbox.checked) { 77 | toFlush += data; 78 | if (data === '\r') { 79 | writer.write(encoder.encode(toFlush)); 80 | writer.releaseLock(); 81 | toFlush = ''; 82 | } 83 | } else { 84 | writer.write(encoder.encode(data)); 85 | } 86 | 87 | writer.releaseLock(); 88 | }); 89 | 90 | /** 91 | * Returns the option corresponding to the given SerialPort if one is present 92 | * in the selection dropdown. 93 | * 94 | * @param {SerialPort} port the port to find 95 | * @return {PortOption} 96 | */ 97 | function findPortOption(port: SerialPort | SerialPortPolyfill): 98 | PortOption | null { 99 | for (let i = 0; i < portSelector.options.length; ++i) { 100 | const option = portSelector.options[i]; 101 | if (option.value === 'prompt') { 102 | continue; 103 | } 104 | const portOption = option as PortOption; 105 | if (portOption.port === port) { 106 | return portOption; 107 | } 108 | } 109 | 110 | return null; 111 | } 112 | 113 | /** 114 | * Adds the given port to the selection dropdown. 115 | * 116 | * @param {SerialPort} port the port to add 117 | * @return {PortOption} 118 | */ 119 | function addNewPort(port: SerialPort | SerialPortPolyfill): PortOption { 120 | const portOption = document.createElement('option') as PortOption; 121 | portOption.textContent = `Port ${portCounter++}`; 122 | portOption.port = port; 123 | portSelector.appendChild(portOption); 124 | return portOption; 125 | } 126 | 127 | /** 128 | * Adds the given port to the selection dropdown, or returns the existing 129 | * option if one already exists. 130 | * 131 | * @param {SerialPort} port the port to add 132 | * @return {PortOption} 133 | */ 134 | function maybeAddNewPort(port: SerialPort | SerialPortPolyfill): PortOption { 135 | const portOption = findPortOption(port); 136 | if (portOption) { 137 | return portOption; 138 | } 139 | 140 | return addNewPort(port); 141 | } 142 | 143 | /** 144 | * Download the terminal's contents to a file. 145 | */ 146 | function downloadTerminalContents(): void { 147 | if (!term) { 148 | throw new Error('no terminal instance found'); 149 | } 150 | 151 | if (term.rows === 0) { 152 | console.log('No output yet'); 153 | return; 154 | } 155 | 156 | term.selectAll(); 157 | const contents = term.getSelection(); 158 | term.clearSelection(); 159 | const linkContent = URL.createObjectURL( 160 | new Blob([new TextEncoder().encode(contents).buffer], 161 | {type: 'text/plain'})); 162 | const fauxLink = document.createElement('a'); 163 | fauxLink.download = `terminal_content_${new Date().getTime()}.txt`; 164 | fauxLink.href = linkContent; 165 | fauxLink.click(); 166 | } 167 | 168 | /** 169 | * Clear the terminal's contents. 170 | */ 171 | function clearTerminalContents(): void { 172 | if (!term) { 173 | throw new Error('no terminal instance found'); 174 | } 175 | 176 | if (term.rows === 0) { 177 | console.log('No output yet'); 178 | return; 179 | } 180 | 181 | term.clear(); 182 | } 183 | 184 | /** 185 | * Sets |port| to the currently selected port. If none is selected then the 186 | * user is prompted for one. 187 | */ 188 | async function getSelectedPort(): Promise { 189 | if (portSelector.value == 'prompt') { 190 | try { 191 | const serial = usePolyfill ? polyfill : navigator.serial; 192 | port = await serial.requestPort({}); 193 | } catch (e) { 194 | return; 195 | } 196 | const portOption = maybeAddNewPort(port); 197 | portOption.selected = true; 198 | } else { 199 | const selectedOption = portSelector.selectedOptions[0] as PortOption; 200 | port = selectedOption.port; 201 | } 202 | } 203 | 204 | /** 205 | * @return {number} the currently selected baud rate 206 | */ 207 | function getSelectedBaudRate(): number { 208 | if (baudRateSelector.value == 'custom') { 209 | return Number.parseInt(customBaudRateInput.value); 210 | } 211 | return Number.parseInt(baudRateSelector.value); 212 | } 213 | 214 | /** 215 | * Resets the UI back to the disconnected state. 216 | */ 217 | function markDisconnected(): void { 218 | term.writeln(''); 219 | portSelector.disabled = false; 220 | connectButton.textContent = 'Connect'; 221 | connectButton.disabled = false; 222 | baudRateSelector.disabled = false; 223 | customBaudRateInput.disabled = false; 224 | dataBitsSelector.disabled = false; 225 | paritySelector.disabled = false; 226 | stopBitsSelector.disabled = false; 227 | flowControlCheckbox.disabled = false; 228 | port = undefined; 229 | } 230 | 231 | /** 232 | * Initiates a connection to the selected port. 233 | */ 234 | async function connectToPort(): Promise { 235 | await getSelectedPort(); 236 | if (!port) { 237 | return; 238 | } 239 | 240 | const options = { 241 | baudRate: getSelectedBaudRate(), 242 | dataBits: Number.parseInt(dataBitsSelector.value), 243 | parity: paritySelector.value as ParityType, 244 | stopBits: Number.parseInt(stopBitsSelector.value), 245 | flowControl: 246 | flowControlCheckbox.checked ? 'hardware' : 'none', 247 | bufferSize, 248 | 249 | // Prior to Chrome 86 these names were used. 250 | baudrate: getSelectedBaudRate(), 251 | databits: Number.parseInt(dataBitsSelector.value), 252 | stopbits: Number.parseInt(stopBitsSelector.value), 253 | rtscts: flowControlCheckbox.checked, 254 | }; 255 | console.log(options); 256 | 257 | portSelector.disabled = true; 258 | connectButton.textContent = 'Connecting...'; 259 | connectButton.disabled = true; 260 | baudRateSelector.disabled = true; 261 | customBaudRateInput.disabled = true; 262 | dataBitsSelector.disabled = true; 263 | paritySelector.disabled = true; 264 | stopBitsSelector.disabled = true; 265 | flowControlCheckbox.disabled = true; 266 | 267 | try { 268 | await port.open(options); 269 | term.writeln(''); 270 | connectButton.textContent = 'Disconnect'; 271 | connectButton.disabled = false; 272 | } catch (e) { 273 | console.error(e); 274 | if (e instanceof Error) { 275 | term.writeln(``); 276 | } 277 | markDisconnected(); 278 | return; 279 | } 280 | 281 | while (port && port.readable) { 282 | try { 283 | try { 284 | reader = port.readable.getReader({mode: 'byob'}); 285 | } catch { 286 | reader = port.readable.getReader(); 287 | } 288 | 289 | let buffer = null; 290 | for (;;) { 291 | const {value, done} = await (async () => { 292 | if (reader instanceof ReadableStreamBYOBReader) { 293 | if (!buffer) { 294 | buffer = new ArrayBuffer(bufferSize); 295 | } 296 | const {value, done} = 297 | await reader.read(new Uint8Array(buffer, 0, bufferSize)); 298 | buffer = value?.buffer; 299 | return {value, done}; 300 | } else { 301 | return await reader.read(); 302 | } 303 | })(); 304 | 305 | if (value) { 306 | await new Promise((resolve) => { 307 | term.write(value, resolve); 308 | }); 309 | } 310 | if (done) { 311 | break; 312 | } 313 | } 314 | } catch (e) { 315 | console.error(e); 316 | await new Promise((resolve) => { 317 | if (e instanceof Error) { 318 | term.writeln(``, resolve); 319 | } 320 | }); 321 | } finally { 322 | if (reader) { 323 | reader.releaseLock(); 324 | reader = undefined; 325 | } 326 | } 327 | } 328 | 329 | if (port) { 330 | try { 331 | await port.close(); 332 | } catch (e) { 333 | console.error(e); 334 | if (e instanceof Error) { 335 | term.writeln(``); 336 | } 337 | } 338 | 339 | markDisconnected(); 340 | } 341 | } 342 | 343 | /** 344 | * Closes the currently active connection. 345 | */ 346 | async function disconnectFromPort(): Promise { 347 | // Move |port| into a local variable so that connectToPort() doesn't try to 348 | // close it on exit. 349 | const localPort = port; 350 | port = undefined; 351 | 352 | if (reader) { 353 | await reader.cancel(); 354 | } 355 | 356 | if (localPort) { 357 | try { 358 | await localPort.close(); 359 | } catch (e) { 360 | console.error(e); 361 | if (e instanceof Error) { 362 | term.writeln(``); 363 | } 364 | } 365 | } 366 | 367 | markDisconnected(); 368 | } 369 | 370 | document.addEventListener('DOMContentLoaded', async () => { 371 | const terminalElement = document.getElementById('terminal'); 372 | if (terminalElement) { 373 | term.open(terminalElement); 374 | fitAddon.fit(); 375 | 376 | window.addEventListener('resize', () => { 377 | fitAddon.fit(); 378 | }); 379 | } 380 | 381 | const downloadOutput = 382 | document.getElementById('download') as HTMLSelectElement; 383 | downloadOutput.addEventListener('click', downloadTerminalContents); 384 | 385 | const clearOutput = document.getElementById('clear') as HTMLSelectElement; 386 | clearOutput.addEventListener('click', clearTerminalContents); 387 | 388 | portSelector = document.getElementById('ports') as HTMLSelectElement; 389 | 390 | connectButton = document.getElementById('connect') as HTMLButtonElement; 391 | connectButton.addEventListener('click', () => { 392 | if (port) { 393 | disconnectFromPort(); 394 | } else { 395 | connectToPort(); 396 | } 397 | }); 398 | 399 | baudRateSelector = document.getElementById('baudrate') as HTMLSelectElement; 400 | baudRateSelector.addEventListener('input', () => { 401 | if (baudRateSelector.value == 'custom') { 402 | customBaudRateInput.hidden = false; 403 | } else { 404 | customBaudRateInput.hidden = true; 405 | } 406 | }); 407 | 408 | customBaudRateInput = 409 | document.getElementById('custom_baudrate') as HTMLInputElement; 410 | dataBitsSelector = document.getElementById('databits') as HTMLSelectElement; 411 | paritySelector = document.getElementById('parity') as HTMLSelectElement; 412 | stopBitsSelector = document.getElementById('stopbits') as HTMLSelectElement; 413 | flowControlCheckbox = document.getElementById('rtscts') as HTMLInputElement; 414 | echoCheckbox = document.getElementById('echo') as HTMLInputElement; 415 | flushOnEnterCheckbox = 416 | document.getElementById('enter_flush') as HTMLInputElement; 417 | autoconnectCheckbox = 418 | document.getElementById('autoconnect') as HTMLInputElement; 419 | 420 | const convertEolCheckbox = 421 | document.getElementById('convert_eol') as HTMLInputElement; 422 | const convertEolCheckboxHandler = () => { 423 | term.options.convertEol = convertEolCheckbox.checked; 424 | }; 425 | convertEolCheckbox.addEventListener('change', convertEolCheckboxHandler); 426 | convertEolCheckboxHandler(); 427 | 428 | const polyfillSwitcher = 429 | document.getElementById('polyfill_switcher') as HTMLAnchorElement; 430 | if (usePolyfill) { 431 | polyfillSwitcher.href = './'; 432 | polyfillSwitcher.textContent = 'Switch to native API'; 433 | } else { 434 | polyfillSwitcher.href = './?polyfill'; 435 | polyfillSwitcher.textContent = 'Switch to API polyfill'; 436 | } 437 | 438 | const serial = usePolyfill ? polyfill : navigator.serial; 439 | const ports: (SerialPort | SerialPortPolyfill)[] = await serial.getPorts(); 440 | ports.forEach((port) => addNewPort(port)); 441 | 442 | // These events are not supported by the polyfill. 443 | // https://github.com/google/web-serial-polyfill/issues/20 444 | if (!usePolyfill) { 445 | navigator.serial.addEventListener('connect', (event) => { 446 | const portOption = addNewPort(event.target as SerialPort); 447 | if (autoconnectCheckbox.checked) { 448 | portOption.selected = true; 449 | connectToPort(); 450 | } 451 | }); 452 | navigator.serial.addEventListener('disconnect', (event) => { 453 | const portOption = findPortOption(event.target as SerialPort); 454 | if (portOption) { 455 | portOption.remove(); 456 | } 457 | }); 458 | } 459 | }); 460 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import eslint from 'vite-plugin-eslint' 2 | import { VitePWA } from 'vite-plugin-pwa' 3 | 4 | export default { 5 | base: './', 6 | plugins: [ 7 | eslint(), 8 | VitePWA({ 9 | registerType: 'autoUpdate', 10 | injectRegister: 'auto', 11 | manifest: { 12 | short_name: 'Serial Terminal', 13 | name: 'Serial Terminal', 14 | icons: [ 15 | { 16 | src: 'images/icons-1024.png', 17 | type: 'image/png', 18 | sizes: '1024x1024', 19 | purpose: 'any maskable' 20 | }, 21 | { 22 | src: 'images/icons-192.png', 23 | type: 'image/png', 24 | sizes: '192x192', 25 | purpose: 'any maskable' 26 | }, 27 | { 28 | src: 'images/icons-512.png', 29 | type: 'image/png', 30 | sizes: '512x512', 31 | purpose: 'any maskable' 32 | } 33 | ], 34 | start_url: './?source=pwa', 35 | display: 'standalone', 36 | scope: './', 37 | id: './' 38 | }, 39 | }) 40 | ] 41 | } 42 | --------------------------------------------------------------------------------