├── .gitignore ├── LICENSE ├── ReadMe.md ├── css └── style.css ├── imgs ├── logo.svg └── main.png ├── index.html └── js ├── ansi_up.min.js └── common.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Web Serial Debug 2 | 3 | 浏览器串口调试工具 4 | 5 | 仅测试了 Edge 和 Chrome 浏览器,其他浏览器未测试是否可用 6 | 7 | 在线体验: [https://itldg.github.io/web-serial-debug/](https://itldg.github.io/web-serial-debug/) 8 | 9 | 国内体验: [https://www.itldg.com/web_serial_debug/](https://www.itldg.com/web_serial_debug/) 10 | 11 | ## 界面预览 12 | 13 | ![界面预览](/imgs/main.png) 14 | 15 | ## 实现功能 16 | 17 | - 自动重连,设备插拔自动重连 18 | - 所有串口参数可设置更改,配置自动保存 19 | - 串口日志支持 HEX, TEXT 和 彩色ANSI,自动滚动 20 | - 分包合并,设定超时时间 21 | - 快捷发送列表,自定义分组,快捷导入导出 22 | - 配置文件导入导出,方便迁移 23 | - 自定义脚本,支持发送和接收数据处理 24 | 25 | ## 使用方法 26 | 27 | 先选择一个电脑连接的串口 28 | 29 | 调整串口参数后打开串口即可开始通讯 30 | 31 | 中间区域是串口日志,可以选择 HEX ,TEXT 或者 彩色ANSI 显示 32 | 33 | 下方是发送区域,可以选择 HEX 或者 TEXT 发送,定时循环发送 34 | 35 | 右侧可以自己添加一些常用指令,快捷发送 36 | 37 | ## 自定义脚本 38 | 39 | 自定义脚本可以在发送和接收数据时进行处理 40 | 41 | 脚本支持 JavaScript 语法,通过`postMessage`和`onmessage`进行通讯 42 | 43 | 如下是一个简单的脚本示例 44 | 45 | ```javascript 46 | addEventListener('message', function ({data}) { 47 | if(data.type=='uart_receive') 48 | { 49 | postMessage({type:'log',data:'消息长度:'+data.data.length}); 50 | //原文答复 51 | postMessage({type:'uart_send',data:data.data}); 52 | } 53 | }) 54 | setInterval(function(){ 55 | //定时发送 56 | postMessage({type:'uart_send_txt',data:'hello world'}); 57 | },1000); 58 | ``` 59 | 60 | `onmessage`接收到的数据格式如下 61 | 62 | ```js 63 | { 64 | "type":"uart_receive", //消息类型 String,目前仅支持 uart_receive 65 | "data":[0,1] //消息内容 Uint8Array 66 | } 67 | ``` 68 | 69 | `postMessage`发送的数据格式如下 70 | 71 | ```js 72 | { 73 | "type":"uart_send", 74 | "data":[0,1] 75 | } 76 | ``` 77 | 78 | | TYPE 类型 | DATA 数据格式 | 说明 | 79 | | ------------- | ------------- | ------------------ | 80 | | uart_send | Uint8Array | 发送字节数据 | 81 | | uart_send_txt | String | 发送文本数据 | 82 | | uart_send_hex | String | 发送十六进制字符串 | 83 | | log | String | 打印日志 | 84 | 85 | 86 | ## 开源 87 | 88 | 代码凌乱不堪,无学习价值 89 | 90 | 希望各位大佬可以协助添砖加瓦,让其更加完善 91 | 92 | 常用的朋友也可以提交一些常用的指令集,后续做一下常用指令集的整理 93 | 94 | 开源地址:[GitHub](https://github.com/itldg/web-serial-debug) | [Gitee](https://gitee.com/itldg/web-serial-debug) 95 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --top-height: 63px; 3 | } 4 | html, 5 | body { 6 | height: 100%; 7 | } 8 | body { 9 | font-size: 0.875rem; 10 | } 11 | #main { 12 | height: calc(100% - var(--top-height)); 13 | } 14 | 15 | #log-main { 16 | display: flex; 17 | flex-direction: column; 18 | height: 100%; 19 | } 20 | #serial-logs { 21 | overflow-y: scroll; 22 | white-space: pre-wrap; 23 | word-break: break-all; 24 | } 25 | #serial-logs.ansi { 26 | background-color: #000; 27 | color: #fff; 28 | } 29 | #serial-options, 30 | #serial-tools { 31 | position: relative; 32 | } 33 | .toggle-button { 34 | align-items: center; 35 | background: none; 36 | border: 0; 37 | display: flex; 38 | flex: none; 39 | font-size: 20px; 40 | height: 100%; 41 | justify-content: center; 42 | width: 20px; 43 | z-index: 1; 44 | background-color: #f8f9fa; 45 | } 46 | .toggle-button:hover { 47 | background: rgba(228, 231, 242, 0.4); 48 | } 49 | 50 | #serial-tools { 51 | height: 100%; 52 | } 53 | #nav-tabContent { 54 | overflow: hidden; 55 | } 56 | #serial-tools .collapse.show, 57 | #nav-quick-send.show { 58 | height: 100%; 59 | display: flex; 60 | flex-direction: column; 61 | } 62 | #nav-code { 63 | height: 100%; 64 | } 65 | #serial-code-editor .CodeMirror { 66 | height: 100%; 67 | } 68 | .CodeMirror-readonly { 69 | background-color: var(--bs-light-rgb); 70 | cursor: not-allowed; 71 | } 72 | -------------------------------------------------------------------------------- /imgs/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /imgs/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itldg/web-serial-debug/c0fc58b9fe9f197f1b660e207c2af237c4cbdc47/imgs/main.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Web Serial Debug-浏览器串口调试工具 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 33 | 34 | 35 | 36 |
37 |
38 |

39 | Web Serial 40 | Web Serial Debug 41 |

42 | 43 | 代码已开源: 44 | Github 45 | Gitee 46 | 隐藏头部 47 | 48 |
49 |
50 | 51 |
52 | 53 | 144 |
145 | 146 |
147 |
149 |

串口日志

150 | 151 |
152 |
153 | 分包超时 154 | 156 |
157 | 158 |
159 | 日志类型 160 | 166 |
167 | 168 |
169 | 170 | 171 | 172 | 173 |
174 |
175 |
176 |
177 |
178 | 180 |
182 |
183 |
184 | 185 | 188 |
189 |
190 | 191 | 194 |
195 |
196 | 197 | 200 |
201 |
202 | 发送间隔(MS) 203 | 205 |
206 |
207 | 209 |
210 |
211 |
212 |
213 | 214 | 215 | 317 |
318 | 319 | 320 | 321 | 336 | 337 | 359 | 360 | 361 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /js/ansi_up.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using Terser v5.19.2. 3 | * Original file: /npm/ansi_up@5.1.0/ansi_up.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | !function(e,t){if("function"==typeof define&&define.amd)define(["exports"],t);else if("object"==typeof exports&&"string"!=typeof exports.nodeName)t(exports);else{var n={};t(n),e.AnsiUp=n.default}}(this,(function(e){"use strict";var t,n=this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e};!function(e){e[e.EOS=0]="EOS",e[e.Text=1]="Text",e[e.Incomplete=2]="Incomplete",e[e.ESC=3]="ESC",e[e.Unknown=4]="Unknown",e[e.SGR=5]="SGR",e[e.OSCURL=6]="OSCURL"}(t||(t={}));var i=function(){function e(){this.VERSION="5.1.0",this.setup_palettes(),this._use_classes=!1,this.bold=!1,this.italic=!1,this.underline=!1,this.fg=this.bg=null,this._buffer="",this._url_whitelist={http:1,https:1}}return Object.defineProperty(e.prototype,"use_classes",{get:function(){return this._use_classes},set:function(e){this._use_classes=e},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"url_whitelist",{get:function(){return this._url_whitelist},set:function(e){this._url_whitelist=e},enumerable:!1,configurable:!0}),e.prototype.setup_palettes=function(){var e=this;this.ansi_colors=[[{rgb:[0,0,0],class_name:"ansi-black"},{rgb:[187,0,0],class_name:"ansi-red"},{rgb:[0,187,0],class_name:"ansi-green"},{rgb:[187,187,0],class_name:"ansi-yellow"},{rgb:[0,0,187],class_name:"ansi-blue"},{rgb:[187,0,187],class_name:"ansi-magenta"},{rgb:[0,187,187],class_name:"ansi-cyan"},{rgb:[255,255,255],class_name:"ansi-white"}],[{rgb:[85,85,85],class_name:"ansi-bright-black"},{rgb:[255,85,85],class_name:"ansi-bright-red"},{rgb:[0,255,0],class_name:"ansi-bright-green"},{rgb:[255,255,85],class_name:"ansi-bright-yellow"},{rgb:[85,85,255],class_name:"ansi-bright-blue"},{rgb:[255,85,255],class_name:"ansi-bright-magenta"},{rgb:[85,255,255],class_name:"ansi-bright-cyan"},{rgb:[255,255,255],class_name:"ansi-bright-white"}]],this.palette_256=[],this.ansi_colors.forEach((function(t){t.forEach((function(t){e.palette_256.push(t)}))}));for(var t=[0,95,135,175,215,255],n=0;n<6;++n)for(var i=0;i<6;++i)for(var s=0;s<6;++s){var r={rgb:[t[n],t[i],t[s]],class_name:"truecolor"};this.palette_256.push(r)}for(var a=8,l=0;l<24;++l,a+=10){var f={rgb:[a,a,a],class_name:"truecolor"};this.palette_256.push(f)}},e.prototype.escape_txt_for_html=function(e){return e.replace(/[&<>"']/gm,(function(e){return"&"===e?"&":"<"===e?"<":">"===e?">":'"'===e?""":"'"===e?"'":void 0}))},e.prototype.append_buffer=function(e){var t=this._buffer+e;this._buffer=t},e.prototype.get_next_packet=function(){var e={kind:t.EOS,text:"",url:""},i=this._buffer.length;if(0==i)return e;var r=this._buffer.indexOf("");if(-1==r)return e.kind=t.Text,e.text=this._buffer,this._buffer="",e;if(r>0)return e.kind=t.Text,e.text=this._buffer.slice(0,r),this._buffer=this._buffer.slice(r),e;if(0==r){if(1==i)return e.kind=t.Incomplete,e;var a=this._buffer.charAt(1);if("["!=a&&"]"!=a)return e.kind=t.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;if("["==a){if(this._csi_regex||(this._csi_regex=s(n(["\n ^ # beginning of line\n #\n # First attempt\n (?: # legal sequence\n [ # CSI\n ([<-?]?) # private-mode char\n ([d;]*) # any digits or semicolons\n ([ -/]? # an intermediate modifier\n [@-~]) # the command\n )\n | # alternate (second attempt)\n (?: # illegal sequence\n [ # CSI\n [ -~]* # anything legal\n ([\0-:]) # anything illegal\n )\n "],["\n ^ # beginning of line\n #\n # First attempt\n (?: # legal sequence\n \\x1b\\[ # CSI\n ([\\x3c-\\x3f]?) # private-mode char\n ([\\d;]*) # any digits or semicolons\n ([\\x20-\\x2f]? # an intermediate modifier\n [\\x40-\\x7e]) # the command\n )\n | # alternate (second attempt)\n (?: # illegal sequence\n \\x1b\\[ # CSI\n [\\x20-\\x7e]* # anything legal\n ([\\x00-\\x1f:]) # anything illegal\n )\n "]))),null===(h=this._buffer.match(this._csi_regex)))return e.kind=t.Incomplete,e;if(h[4])return e.kind=t.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;""!=h[1]||"m"!=h[3]?e.kind=t.Unknown:e.kind=t.SGR,e.text=h[2];var l=h[0].length;return this._buffer=this._buffer.slice(l),e}if("]"==a){if(i<4)return e.kind=t.Incomplete,e;if("8"!=this._buffer.charAt(2)||";"!=this._buffer.charAt(3))return e.kind=t.ESC,e.text=this._buffer.slice(0,1),this._buffer=this._buffer.slice(1),e;this._osc_st||(this._osc_st=function(e){for(var t=[],n=1;n0;){var n=t.shift(),i=parseInt(n,10);if(isNaN(i)||0===i)this.fg=this.bg=null,this.bold=!1,this.italic=!1,this.underline=!1;else if(1===i)this.bold=!0;else if(3===i)this.italic=!0;else if(4===i)this.underline=!0;else if(22===i)this.bold=!1;else if(23===i)this.italic=!1;else if(24===i)this.underline=!1;else if(39===i)this.fg=null;else if(49===i)this.bg=null;else if(i>=30&&i<38)this.fg=this.ansi_colors[0][i-30];else if(i>=40&&i<48)this.bg=this.ansi_colors[0][i-40];else if(i>=90&&i<98)this.fg=this.ansi_colors[1][i-90];else if(i>=100&&i<108)this.bg=this.ansi_colors[1][i-100];else if((38===i||48===i)&&t.length>0){var s=38===i,r=t.shift();if("5"===r&&t.length>0){var a=parseInt(t.shift(),10);a>=0&&a<=255&&(s?this.fg=this.palette_256[a]:this.bg=this.palette_256[a])}if("2"===r&&t.length>2){var l=parseInt(t.shift(),10),f=parseInt(t.shift(),10),h=parseInt(t.shift(),10);if(l>=0&&l<=255&&f>=0&&f<=255&&h>=0&&h<=255){var o={rgb:[l,f,h],class_name:"truecolor"};s?this.fg=o:this.bg=o}}}}},e.prototype.transform_to_html=function(e){var t=e.text;if(0===t.length)return t;if(t=this.escape_txt_for_html(t),!e.bold&&!e.italic&&!e.underline&&null===e.fg&&null===e.bg)return t;var n=[],i=[],s=e.fg,r=e.bg;e.bold&&n.push("font-weight:bold"),e.italic&&n.push("font-style:italic"),e.underline&&n.push("text-decoration:underline"),this._use_classes?(s&&("truecolor"!==s.class_name?i.push(s.class_name+"-fg"):n.push("color:rgb("+s.rgb.join(",")+")")),r&&("truecolor"!==r.class_name?i.push(r.class_name+"-bg"):n.push("background-color:rgb("+r.rgb.join(",")+")"))):(s&&n.push("color:rgb("+s.rgb.join(",")+")"),r&&n.push("background-color:rgb("+r.rgb+")"));var a="",l="";return i.length&&(a=' class="'+i.join(" ")+'"'),n.length&&(l=' style="'+n.join(";")+'"'),""+t+""},e.prototype.process_hyperlink=function(e){var t=e.url.split(":");return t.length<1?"":this._url_whitelist[t[0]]?''+this.escape_txt_for_html(e.text)+"":""},e}();function s(e){for(var t=[],n=1;n { 7 | if (ports.length > 0) { 8 | serialPort = ports[0] 9 | serialStatuChange(true) 10 | } 11 | }) 12 | let reader 13 | //串口目前是打开状态 14 | let serialOpen = false 15 | //串口目前是手动关闭状态 16 | let serialClose = true 17 | //串口分包合并时钟 18 | let serialTimer = null 19 | //串口循环发送时钟 20 | let serialloopSendTimer = null 21 | //串口缓存数据 22 | let serialData = [] 23 | //文本解码 24 | let textdecoder = new TextDecoder() 25 | let currQuickSend = [] 26 | //快捷发送列表 27 | let quickSendList = [ 28 | { 29 | name: 'ESP32 AT指令', 30 | list: [ 31 | { 32 | name: '测试 AT 启动', 33 | content: 'AT', 34 | hex: false, 35 | }, 36 | { 37 | name: '重启模块', 38 | content: 'AT+RST', 39 | hex: false, 40 | }, 41 | { 42 | name: '查看版本信息', 43 | content: 'AT+GMR', 44 | hex: false, 45 | }, 46 | { 47 | name: '查询当前固件支持的所有命令及命令类型', 48 | content: 'AT+CMD?', 49 | hex: false, 50 | }, 51 | { 52 | name: '进⼊ Deep-sleep 模式 1分钟', 53 | content: 'AT+GSLP=60000', 54 | hex: false, 55 | }, 56 | { 57 | name: '开启AT回显功能', 58 | content: 'ATE1', 59 | hex: false, 60 | }, 61 | { 62 | name: '关闭AT回显功能', 63 | content: 'ATE0', 64 | hex: false, 65 | }, 66 | { 67 | name: '恢复出厂设置', 68 | content: 'AT+RESTORE', 69 | hex: false, 70 | }, 71 | { 72 | name: '查询 UART 当前临时配置', 73 | content: 'AT+UART_CUR?', 74 | hex: false, 75 | }, 76 | { 77 | name: '设置 UART 115200 保存flash', 78 | content: 'AT+UART_DEF=115200,8,1,0,3', 79 | hex: false, 80 | }, 81 | { 82 | name: '查询 sleep 模式', 83 | content: 'AT+SLEEP?', 84 | hex: false, 85 | }, 86 | { 87 | name: '查询当前剩余堆空间和最小堆空间', 88 | content: 'AT+SYSRAM?', 89 | hex: false, 90 | }, 91 | { 92 | name: '查询系统提示信息', 93 | content: 'AT+SYSMSG?', 94 | hex: false, 95 | }, 96 | { 97 | name: '查询 flash 用户分区', 98 | content: 'AT+SYSFLASH?', 99 | hex: false, 100 | }, 101 | { 102 | name: '查询本地时间戳', 103 | content: 'AT+SYSTIMESTAMP?', 104 | hex: false, 105 | }, 106 | { 107 | name: '查询 AT 错误代码提示', 108 | content: 'AT+SYSLOG?', 109 | hex: false, 110 | }, 111 | { 112 | name: '设置/查询系统参数存储模式', 113 | content: 'AT+SYSPARA?', 114 | hex: false, 115 | }, 116 | ], 117 | }, 118 | ] 119 | let worker = null 120 | //工具配置 121 | let toolOptions = { 122 | //自动滚动 123 | autoScroll: true, 124 | //显示时间 界面未开放 125 | showTime: true, 126 | //日志类型 127 | logType: 'hex&text', 128 | //分包合并时间 129 | timeOut: 50, 130 | //末尾加回车换行 131 | addCRLF: false, 132 | //HEX发送 133 | hexSend: false, 134 | //循环发送 135 | loopSend: false, 136 | //循环发送时间 137 | loopSendTime: 1000, 138 | //输入的发送内容 139 | sendContent: '', 140 | //快捷发送选中索引 141 | quickSendIndex: 0, 142 | } 143 | 144 | //生成快捷发送列表 145 | let quickSend = document.getElementById('serial-quick-send') 146 | let sendList = localStorage.getItem('quickSendList') 147 | if (sendList) { 148 | quickSendList = JSON.parse(sendList) 149 | } 150 | quickSendList.forEach((item, index) => { 151 | let option = document.createElement('option') 152 | option.innerText = item.name 153 | option.value = index 154 | quickSend.appendChild(option) 155 | }) 156 | 157 | //快捷发送列表被单击 158 | document.getElementById('serial-quick-send-content').addEventListener('click', (e) => { 159 | let curr = e.target 160 | if (curr.tagName != 'BUTTON') { 161 | curr = curr.parentNode 162 | } 163 | if (curr.tagName != 'BUTTON') { 164 | return 165 | } 166 | const index = Array.from(curr.parentNode.parentNode.children).indexOf(curr.parentNode) 167 | if (curr.classList.contains('quick-remove')) { 168 | currQuickSend.list.splice(index, 1) 169 | curr.parentNode.remove() 170 | saveQuickList() 171 | return 172 | } 173 | if (curr.classList.contains('quick-send')) { 174 | let item = currQuickSend.list[index] 175 | if (item.hex) { 176 | sendHex(item.content) 177 | return 178 | } 179 | sendText(item.content) 180 | } 181 | }) 182 | //快捷列表双击改名 183 | document.getElementById('serial-quick-send-content').addEventListener('dblclick', (e) => { 184 | let curr = e.target 185 | if (curr.tagName != 'INPUT' || curr.type != 'text') { 186 | return 187 | } 188 | const index = Array.from(curr.parentNode.parentNode.children).indexOf(curr.parentNode) 189 | changeName((name) => { 190 | currQuickSend.list[index].name = name 191 | curr.parentNode.outerHTML = getQuickItemHtml(currQuickSend.list[index]) 192 | saveQuickList() 193 | }, currQuickSend.list[index].name) 194 | }) 195 | //快捷发送列表被改变 196 | document.getElementById('serial-quick-send-content').addEventListener('change', (e) => { 197 | let curr = e.target 198 | if (curr.tagName != 'INPUT') { 199 | return 200 | } 201 | const index = Array.from(curr.parentNode.parentNode.children).indexOf(curr.parentNode) 202 | if (curr.type == 'text') { 203 | currQuickSend.list[index].content = curr.value 204 | } 205 | if (curr.type == 'checkbox') { 206 | currQuickSend.list[index].hex = curr.checked 207 | } 208 | saveQuickList() 209 | }) 210 | function saveQuickList() { 211 | localStorage.setItem('quickSendList', JSON.stringify(quickSendList)) 212 | } 213 | 214 | const quickSendContent = document.getElementById('serial-quick-send-content') 215 | //快捷发送列表更换选项 216 | quickSend.addEventListener('change', (e) => { 217 | let index = e.target.value 218 | if (index != -1) { 219 | changeOption('quickSendIndex', index) 220 | currQuickSend = quickSendList[index] 221 | // 222 | quickSendContent.innerHTML = '' 223 | currQuickSend.list.forEach((item) => { 224 | quickSendContent.innerHTML += getQuickItemHtml(item) 225 | }) 226 | } 227 | }) 228 | //添加快捷发送 229 | document.getElementById('serial-quick-send-add').addEventListener('click', (e) => { 230 | const item = { 231 | name: '发送', 232 | content: '', 233 | hex: false, 234 | } 235 | currQuickSend.list.push(item) 236 | quickSendContent.innerHTML += getQuickItemHtml(item) 237 | saveQuickList() 238 | }) 239 | function getQuickItemHtml(item) { 240 | return `
241 | 242 | 243 | 244 | 245 |
` 246 | } 247 | //快捷发送分组新增 248 | document.getElementById('serial-quick-send-add-group').addEventListener('click', (e) => { 249 | changeName((name) => { 250 | quickSendList.push({ 251 | name: name, 252 | list: [], 253 | }) 254 | quickSend.innerHTML += `` 255 | quickSend.value = quickSendList.length - 1 256 | quickSend.dispatchEvent(new Event('change')) 257 | saveQuickList() 258 | }) 259 | }) 260 | //快捷发送分组重命名 261 | document.getElementById('serial-quick-send-rename-group').addEventListener('click', (e) => { 262 | changeName((name) => { 263 | currQuickSend.name = name 264 | quickSend.options[quickSend.value].innerText = name 265 | saveQuickList() 266 | }, currQuickSend.name) 267 | }) 268 | //快捷发送分组删除 269 | document.getElementById('serial-quick-send-remove-group').addEventListener('click', (e) => { 270 | if (quickSendList.length == 1) { 271 | return 272 | } 273 | //弹窗询问是否删除 274 | if (!confirm('是否删除该分组?')) { 275 | return 276 | } 277 | quickSendList.splice(quickSend.value, 1) 278 | quickSend.options[quickSend.value].remove() 279 | quickSend.value = 0 280 | quickSend.dispatchEvent(new Event('change')) 281 | saveQuickList() 282 | }) 283 | 284 | //导出 285 | document.getElementById('serial-quick-send-export').addEventListener('click', (e) => { 286 | let data = JSON.stringify(currQuickSend.list) 287 | let blob = new Blob([data], { type: 'text/plain' }) 288 | saveAs(blob, currQuickSend.name + '.json') 289 | }) 290 | //导入 291 | document.getElementById('serial-quick-send-import-btn').addEventListener('click', (e) => { 292 | document.getElementById('serial-quick-send-import').click() 293 | }) 294 | document.getElementById('serial-quick-send-import').addEventListener('change', (e) => { 295 | let file = e.target.files[0] 296 | e.target.value = '' 297 | let reader = new FileReader() 298 | reader.onload = function (e) { 299 | let data = e.target.result 300 | try { 301 | let list = JSON.parse(data) 302 | currQuickSend.list.push(...list) 303 | list.forEach((item) => { 304 | quickSendContent.innerHTML += getQuickItemHtml(item) 305 | }) 306 | saveQuickList() 307 | } catch (e) { 308 | showMsg('导入失败:' + e.message) 309 | } 310 | } 311 | reader.readAsText(file) 312 | }) 313 | //重置参数 314 | document.getElementById('serial-reset').addEventListener('click', (e) => { 315 | if (!confirm('是否重置参数?')) { 316 | return 317 | } 318 | localStorage.removeItem('serialOptions') 319 | localStorage.removeItem('toolOptions') 320 | localStorage.removeItem('quickSendList') 321 | localStorage.removeItem('code') 322 | location.reload() 323 | }) 324 | //导出参数 325 | document.getElementById('serial-export').addEventListener('click', (e) => { 326 | let data = { 327 | serialOptions: localStorage.getItem('serialOptions'), 328 | toolOptions: localStorage.getItem('toolOptions'), 329 | quickSendList: localStorage.getItem('quickSendList'), 330 | code: localStorage.getItem('code'), 331 | } 332 | let blob = new Blob([JSON.stringify(data)], { type: 'text/plain' }) 333 | saveAs(blob, 'web-serial-debug.json') 334 | }) 335 | //导入参数 336 | document.getElementById('serial-import').addEventListener('click', (e) => { 337 | document.getElementById('serial-import-file').click() 338 | }) 339 | function setParam(key, value) { 340 | if (value == null) { 341 | localStorage.removeItem(key) 342 | } else { 343 | localStorage.setItem(key, value) 344 | } 345 | } 346 | document.getElementById('serial-import-file').addEventListener('change', (e) => { 347 | let file = e.target.files[0] 348 | e.target.value = '' 349 | let reader = new FileReader() 350 | reader.onload = function (e) { 351 | let data = e.target.result 352 | try { 353 | let obj = JSON.parse(data) 354 | setParam('serialOptions', obj.serialOptions) 355 | setParam('toolOptions', obj.toolOptions) 356 | setParam('quickSendList', obj.quickSendList) 357 | setParam('code', obj.code) 358 | location.reload() 359 | } catch (e) { 360 | showMsg('导入失败:' + e.message) 361 | } 362 | } 363 | reader.readAsText(file) 364 | }) 365 | const serialCodeContent = document.getElementById('serial-code-content') 366 | const serialCodeSelect = document.getElementById('serial-code-select') 367 | const code = localStorage.getItem('code') 368 | if (code) { 369 | serialCodeContent.value = code 370 | } 371 | //代码编辑器 372 | var editor = CodeMirror.fromTextArea(serialCodeContent, { 373 | lineNumbers: true, // 显示行数 374 | indentUnit: 4, // 缩进单位为4 375 | styleActiveLine: true, // 当前行背景高亮 376 | matchBrackets: true, // 括号匹配 377 | mode: 'javascript', // 设置编辑器语言为JavaScript 378 | // lineWrapping: true, // 自动换行 379 | theme: 'idea', // 主题 380 | }) 381 | //读取本地文件 382 | serialCodeSelect.onchange = function (e) { 383 | var fr = new FileReader() 384 | fr.onload = function () { 385 | editor.setValue(fr.result) 386 | } 387 | fr.readAsText(this.files[0]) 388 | } 389 | document.getElementById('serial-code-load').onclick = function () { 390 | serialCodeSelect.click() 391 | } 392 | //运行或停止脚本 393 | const code_editor_run = document.getElementById('serial-code-run') 394 | code_editor_run.addEventListener('click', (e) => { 395 | if (worker) { 396 | worker.terminate() 397 | worker = null 398 | code_editor_run.innerHTML = '运行' 399 | editor.setOption('readOnly', false) 400 | editor.getWrapperElement().classList.remove('CodeMirror-readonly') 401 | return 402 | } 403 | editor.setOption('readOnly', 'nocursor') 404 | editor.getWrapperElement().classList.add('CodeMirror-readonly') 405 | localStorage.setItem('code', editor.getValue()) 406 | code_editor_run.innerHTML = '停止' 407 | var blob = new Blob([editor.getValue()], { type: 'text/javascript' }) 408 | worker = new Worker(window.URL.createObjectURL(blob)) 409 | worker.onmessage = function (e) { 410 | if (e.data.type == 'uart_send') { 411 | writeData(new Uint8Array(e.data.data)) 412 | } else if (e.data.type == 'uart_send_hex') { 413 | sendHex(e.data.data) 414 | } else if (e.data.type == 'uart_send_txt') { 415 | sendText(e.data.data) 416 | } else if (e.data.type == 'log') { 417 | addLogErr(e.data.data) 418 | } 419 | } 420 | }) 421 | //读取参数 422 | let options = localStorage.getItem('serialOptions') 423 | if (options) { 424 | let serialOptions = JSON.parse(options) 425 | set('serial-baud', serialOptions.baudRate) 426 | set('serial-data-bits', serialOptions.dataBits) 427 | set('serial-stop-bits', serialOptions.stopBits) 428 | set('serial-parity', serialOptions.parity) 429 | set('serial-buffer-size', serialOptions.bufferSize) 430 | set('serial-flow-control', serialOptions.flowControl) 431 | } 432 | options = localStorage.getItem('toolOptions') 433 | if (options) { 434 | toolOptions = JSON.parse(options) 435 | } 436 | document.getElementById('serial-timer-out').value = toolOptions.timeOut 437 | document.getElementById('serial-log-type').value = toolOptions.logType 438 | document.getElementById('serial-auto-scroll').innerText = toolOptions.autoScroll ? '自动滚动' : '暂停滚动' 439 | document.getElementById('serial-add-crlf').checked = toolOptions.addCRLF 440 | document.getElementById('serial-hex-send').checked = toolOptions.hexSend 441 | document.getElementById('serial-loop-send').checked = toolOptions.loopSend 442 | document.getElementById('serial-loop-send-time').value = toolOptions.loopSendTime 443 | document.getElementById('serial-send-content').value = toolOptions.sendContent 444 | quickSend.value = toolOptions.quickSendIndex 445 | quickSend.dispatchEvent(new Event('change')) 446 | resetLoopSend() 447 | 448 | //实时修改选项 449 | document.getElementById('serial-timer-out').addEventListener('change', (e) => { 450 | changeOption('timeOut', parseInt(e.target.value)) 451 | }) 452 | document.getElementById('serial-log-type').addEventListener('change', (e) => { 453 | changeOption('logType', e.target.value) 454 | if (e.target.value.includes('ansi')) { 455 | serialLogs.classList.add('ansi') 456 | } else { 457 | serialLogs.classList.remove('ansi') 458 | } 459 | }) 460 | document.getElementById('serial-auto-scroll').addEventListener('click', function (e) { 461 | let autoScroll = this.innerText != '自动滚动' 462 | this.innerText = autoScroll ? '自动滚动' : '暂停滚动' 463 | changeOption('autoScroll', autoScroll) 464 | }) 465 | document.getElementById('serial-send-content').addEventListener('change', function (e) { 466 | changeOption('sendContent', this.value) 467 | }) 468 | document.getElementById('serial-add-crlf').addEventListener('change', function (e) { 469 | changeOption('addCRLF', this.checked) 470 | }) 471 | document.getElementById('serial-hex-send').addEventListener('change', function (e) { 472 | changeOption('hexSend', this.checked) 473 | }) 474 | document.getElementById('serial-loop-send').addEventListener('change', function (e) { 475 | changeOption('loopSend', this.checked) 476 | resetLoopSend() 477 | }) 478 | document.getElementById('serial-loop-send-time').addEventListener('change', function (e) { 479 | changeOption('loopSendTime', parseInt(this.value)) 480 | resetLoopSend() 481 | }) 482 | 483 | document.querySelectorAll('#serial-options .input-group input,#serial-options .input-group select').forEach((item) => { 484 | item.addEventListener('change', async (e) => { 485 | if (!serialOpen) { 486 | return 487 | } 488 | //未找到API可以动态修改串口参数,先关闭再重新打开 489 | await closeSerial() 490 | //立即打开会提示串口已打开,延迟50ms再打开 491 | setTimeout(() => { 492 | openSerial() 493 | }, 50) 494 | }) 495 | }) 496 | 497 | //重制发送循环时钟 498 | function resetLoopSend() { 499 | clearInterval(serialloopSendTimer) 500 | if (toolOptions.loopSend) { 501 | serialloopSendTimer = setInterval(() => { 502 | send() 503 | }, toolOptions.loopSendTime) 504 | } 505 | } 506 | 507 | //清空 508 | document.getElementById('serial-clear').addEventListener('click', (e) => { 509 | serialLogs.innerHTML = '' 510 | }) 511 | //复制 512 | document.getElementById('serial-copy').addEventListener('click', (e) => { 513 | let text = serialLogs.innerText 514 | if (text) { 515 | copyText(text) 516 | } 517 | }) 518 | //保存 519 | document.getElementById('serial-save').addEventListener('click', (e) => { 520 | let text = serialLogs.innerText 521 | if (text) { 522 | saveText(text) 523 | } 524 | }) 525 | //发送 526 | document.getElementById('serial-send').addEventListener('click', (e) => { 527 | send() 528 | }) 529 | 530 | const serialToggle = document.getElementById('serial-open-or-close') 531 | const serialLogs = document.getElementById('serial-logs') 532 | 533 | //选择串口 534 | document.getElementById('serial-select-port').addEventListener('click', async () => { 535 | // 客户端授权 536 | try { 537 | await navigator.serial.requestPort().then(async (port) => { 538 | closeSerial() 539 | serialPort = port 540 | serialStatuChange(true) 541 | }) 542 | } catch (e) { 543 | console.error('获取串口权限出错' + e.toString()) 544 | } 545 | }) 546 | 547 | //关闭串口 548 | async function closeSerial() { 549 | if (serialOpen) { 550 | serialOpen = false 551 | reader?.cancel() 552 | serialToggle.innerHTML = '打开串口' 553 | } 554 | } 555 | 556 | //打开串口 557 | async function openSerial() { 558 | let SerialOptions = { 559 | baudRate: parseInt(get('serial-baud')), 560 | dataBits: parseInt(get('serial-data-bits')), 561 | stopBits: parseInt(get('serial-stop-bits')), 562 | parity: get('serial-parity'), 563 | bufferSize: parseInt(get('serial-buffer-size')), 564 | flowControl: get('serial-flow-control'), 565 | } 566 | // console.log('串口配置', JSON.stringify(SerialOptions)) 567 | serialPort 568 | .open(SerialOptions) 569 | .then(() => { 570 | serialToggle.innerHTML = '关闭串口' 571 | serialOpen = true 572 | serialClose = false 573 | localStorage.setItem('serialOptions', JSON.stringify(SerialOptions)) 574 | readData() 575 | }) 576 | .catch((e) => { 577 | showMsg('打开串口失败:' + e.toString()) 578 | }) 579 | } 580 | 581 | //打开或关闭串口 582 | serialToggle.addEventListener('click', async () => { 583 | if (!serialPort) { 584 | showMsg('请先选择串口') 585 | return 586 | } 587 | 588 | if (serialPort.writable && serialPort.readable) { 589 | closeSerial() 590 | serialClose = true 591 | return 592 | } 593 | 594 | openSerial() 595 | }) 596 | 597 | //设置读取元素 598 | function get(id) { 599 | return document.getElementById(id).value 600 | } 601 | function set(id, value) { 602 | return (document.getElementById(id).value = value) 603 | } 604 | 605 | //修改参数并保存 606 | function changeOption(key, value) { 607 | toolOptions[key] = value 608 | localStorage.setItem('toolOptions', JSON.stringify(toolOptions)) 609 | } 610 | 611 | //串口事件监听 612 | navigator.serial.addEventListener('connect', (e) => { 613 | serialStatuChange(true) 614 | serialPort = e.target 615 | //未主动关闭连接的情况下,设备重插,自动重连 616 | if (!serialClose) { 617 | openSerial() 618 | } 619 | }) 620 | navigator.serial.addEventListener('disconnect', (e) => { 621 | serialStatuChange(false) 622 | setTimeout(closeSerial, 500) 623 | }) 624 | function serialStatuChange(statu) { 625 | let tip 626 | if (statu) { 627 | tip = '' 628 | } else { 629 | tip = '' 630 | } 631 | document.getElementById('serial-status').innerHTML = tip 632 | } 633 | //串口数据收发 634 | async function send() { 635 | let content = document.getElementById('serial-send-content').value 636 | if (!content) { 637 | addLogErr('发送内容为空') 638 | return 639 | } 640 | if (toolOptions.hexSend) { 641 | await sendHex(content) 642 | } else { 643 | await sendText(content) 644 | } 645 | } 646 | 647 | //发送HEX到串口 648 | async function sendHex(hex) { 649 | const value = hex.replace(/\s+/g, '') 650 | if (/^[0-9A-Fa-f]+$/.test(value) && value.length % 2 === 0) { 651 | let data = [] 652 | for (let i = 0; i < value.length; i = i + 2) { 653 | data.push(parseInt(value.substring(i, i + 2), 16)) 654 | } 655 | await writeData(Uint8Array.from(data)) 656 | } else { 657 | addLogErr('HEX格式错误:' + hex) 658 | } 659 | } 660 | 661 | //发送文本到串口 662 | async function sendText(text) { 663 | const encoder = new TextEncoder() 664 | writeData(encoder.encode(text)) 665 | } 666 | 667 | //写串口数据 668 | async function writeData(data) { 669 | if (!serialPort || !serialPort.writable) { 670 | addLogErr('请先打开串口再发送数据') 671 | return 672 | } 673 | const writer = serialPort.writable.getWriter() 674 | if (toolOptions.addCRLF) { 675 | data = new Uint8Array([...data, 0x0d, 0x0a]) 676 | } 677 | await writer.write(data) 678 | writer.releaseLock() 679 | addLog(data, false) 680 | } 681 | 682 | //读串口数据 683 | async function readData() { 684 | while (serialOpen && serialPort.readable) { 685 | reader = serialPort.readable.getReader() 686 | try { 687 | while (true) { 688 | const { value, done } = await reader.read() 689 | if (done) { 690 | break 691 | } 692 | dataReceived(value) 693 | } 694 | } catch (error) { 695 | } finally { 696 | reader.releaseLock() 697 | } 698 | } 699 | await serialPort.close() 700 | } 701 | 702 | //串口分包合并 703 | function dataReceived(data) { 704 | serialData.push(...data) 705 | if (toolOptions.timeOut == 0) { 706 | if (worker) { 707 | worker.postMessage({ type: 'uart_receive', data: serialData }) 708 | } 709 | addLog(serialData, true) 710 | serialData = [] 711 | return 712 | } 713 | //清除之前的时钟 714 | clearTimeout(serialTimer) 715 | serialTimer = setTimeout(() => { 716 | if (worker) { 717 | worker.postMessage({ type: 'uart_receive', data: serialData }) 718 | } 719 | //超时发出 720 | addLog(serialData, true) 721 | serialData = [] 722 | }, toolOptions.timeOut) 723 | } 724 | var ansi_up = new AnsiUp() 725 | //添加日志 726 | function addLog(data, isReceive = true) { 727 | let classname = 'text-primary' 728 | let form = '→' 729 | if (isReceive) { 730 | classname = 'text-success' 731 | form = '←' 732 | } 733 | newmsg = '' 734 | if (toolOptions.logType.includes('hex')) { 735 | let dataHex = [] 736 | for (const d of data) { 737 | //转16进制并补0 738 | dataHex.push(('0' + d.toString(16).toLocaleUpperCase()).slice(-2)) 739 | } 740 | if (toolOptions.logType.includes('&')) { 741 | newmsg += 'HEX:' 742 | } 743 | newmsg += dataHex.join(' ') + '
' 744 | } 745 | if (toolOptions.logType.includes('text')) { 746 | let dataText = textdecoder.decode(Uint8Array.from(data)) 747 | if (toolOptions.logType.includes('&')) { 748 | newmsg += 'TEXT:' 749 | } 750 | //转义HTML标签,防止内容被当作标签渲染 751 | newmsg += HTMLEncode(dataText) 752 | } 753 | if (toolOptions.logType.includes('ansi')) { 754 | const dataText = textdecoder.decode(Uint8Array.from(data)) 755 | const html = ansi_up.ansi_to_html(dataText) 756 | newmsg += html 757 | } 758 | let time = toolOptions.showTime ? formatDate(new Date()) + ' ' : '' 759 | const template = '
' + time + form + '
' + newmsg + '
' 760 | let tempNode = document.createElement('div') 761 | tempNode.innerHTML = template 762 | serialLogs.append(tempNode) 763 | if (toolOptions.autoScroll) { 764 | serialLogs.scrollTop = serialLogs.scrollHeight - serialLogs.clientHeight 765 | } 766 | } 767 | //HTML转义 768 | function HTMLEncode(html) { 769 | var temp = document.createElement('div') 770 | temp.textContent != null ? (temp.textContent = html) : (temp.innerText = html) 771 | var output = temp.innerHTML 772 | temp = null 773 | return output 774 | } 775 | //HTML反转义 776 | function HTMLDecode(text) { 777 | var temp = document.createElement('div') 778 | temp.innerHTML = text 779 | var output = temp.innerText || temp.textContent 780 | temp = null 781 | return output 782 | } 783 | //系统日志 784 | function addLogErr(msg) { 785 | let time = toolOptions.showTime ? formatDate(new Date()) + ' ' : '' 786 | const template = '
' + time + ' 系统消息
' + msg + '
' 787 | let tempNode = document.createElement('div') 788 | tempNode.innerHTML = template 789 | serialLogs.append(tempNode) 790 | if (toolOptions.autoScroll) { 791 | serialLogs.scrollTop = serialLogs.scrollHeight - serialLogs.clientHeight 792 | } 793 | } 794 | 795 | //复制文本 796 | function copyText(text) { 797 | let textarea = document.createElement('textarea') 798 | textarea.value = text 799 | textarea.readOnly = 'readonly' 800 | textarea.style.position = 'absolute' 801 | textarea.style.left = '-9999px' 802 | document.body.appendChild(textarea) 803 | textarea.select() 804 | textarea.setSelectionRange(0, textarea.value.length) 805 | document.execCommand('copy') 806 | document.body.removeChild(textarea) 807 | showMsg('已复制到剪贴板') 808 | } 809 | 810 | //保存文本 811 | function saveText(text) { 812 | let blob = new Blob([text], { type: 'text/plain;charset=utf-8' }) 813 | saveAs(blob, 'serial.log') 814 | } 815 | 816 | //下载文件 817 | function saveAs(blob, filename) { 818 | if (window.navigator.msSaveOrOpenBlob) { 819 | navigator.msSaveBlob(blob, filename) 820 | } else { 821 | let link = document.createElement('a') 822 | let body = document.querySelector('body ') 823 | link.href = window.URL.createObjectURL(blob) 824 | link.download = filename 825 | // fix Firefox 826 | link.style.display = 'none' 827 | body.appendChild(link) 828 | link.click() 829 | body.removeChild(link) 830 | window.URL.revokeObjectURL(link.href) 831 | } 832 | } 833 | 834 | //弹窗 835 | const modalTip = new bootstrap.Modal('#model-tip') 836 | function showMsg(msg, title = 'Web Serial') { 837 | //alert(msg) 838 | document.getElementById('modal-title').innerHTML = title 839 | document.getElementById('modal-message').innerHTML = msg 840 | modalTip.show() 841 | } 842 | 843 | //当前时间 精确到毫秒 844 | function formatDate(now) { 845 | const hour = now.getHours() < 10 ? '0' + now.getHours() : now.getHours() 846 | const minute = now.getMinutes() < 10 ? '0' + now.getMinutes() : now.getMinutes() 847 | const second = now.getSeconds() < 10 ? '0' + now.getSeconds() : now.getSeconds() 848 | const millisecond = ('00' + now.getMilliseconds()).slice(-3) 849 | return `${hour}:${minute}:${second}.${millisecond}` 850 | } 851 | 852 | //左右折叠 853 | document.querySelectorAll('.toggle-button').forEach((element) => { 854 | element.addEventListener('click', (e) => { 855 | e.currentTarget.parentElement.querySelector('.collapse').classList.toggle('show') 856 | e.currentTarget.querySelector('i').classList.toggle('bi-chevron-compact-right') 857 | e.currentTarget.querySelector('i').classList.toggle('bi-chevron-compact-left') 858 | }) 859 | }) 860 | 861 | //设置名称 862 | const modalNewName = new bootstrap.Modal('#model-change-name') 863 | function changeName(callback, oldName = '') { 864 | set('model-new-name', oldName) 865 | modalNewName.show() 866 | document.getElementById('model-save-name').onclick = null 867 | document.getElementById('model-save-name').onclick = function () { 868 | callback(get('model-new-name')) 869 | modalNewName.hide() 870 | } 871 | } 872 | })() 873 | --------------------------------------------------------------------------------