├── LICENSE.txt ├── README.md ├── media ├── favicon.ico ├── images │ ├── logo-color.png │ └── logo-color.svg ├── index.html ├── lib │ ├── uPlot.iife.min.js │ ├── uPlot.min.css │ ├── uplot-component.js │ └── vue.js ├── main.cpp ├── main.js └── style.css ├── package.json ├── preview.png ├── src └── extension.ts └── tsconfig.json /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexandre Brehmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repository deprecated 2 | This repository has been merged with [the main Teleplot repo](https://github.com/nesnes/teleplot) and won't be updated anymore. 3 | 4 | The VSCode extension of teleplot will now be maintained from [the main Teleplot repo](https://github.com/nesnes/teleplot) 5 | 6 | # Please go [there](https://github.com/nesnes/teleplot) instead. 7 | 8 | # Teleplot for VSCode 9 | 10 | Plots telemetry sent over **Serial** or **UDP Packets**. 11 | 12 | ![](preview.png) 13 | 14 | ## From an Arduino (Serial) 15 | 16 | ```c++ 17 | #include 18 | 19 | void setup() { 20 | Serial.begin(115200); 21 | // Print log 22 | Serial.println("setup"); 23 | } 24 | 25 | float i=0; 26 | void loop() { 27 | i+=0.1; 28 | 29 | // Print log 30 | Serial.print("loop"); 31 | Serial.println(i); 32 | 33 | // Plot a sinus 34 | Serial.print(">sin:"); 35 | Serial.println(sin(i)); 36 | 37 | // Plot a cosinus 38 | Serial.print(">cos:"); 39 | Serial.println(cos(i)); 40 | 41 | delay(50); 42 | } 43 | ``` 44 | 45 | Every **serial** message formated `>varName:1234\n` will be ploted in teleplot. Other messages will be printed in the teleplot console. 46 | 47 | Serial port needs to be selected and connected at the top-left on Teleplot. 48 | 49 | > This format is **specific** to **Serial** messages to enhance ease of use on microcontrollers. 50 | 51 | 52 | ## From any program (UDP) 53 | 54 | Teleplot listen to UDP packects on port `47269`, allowing any type of software to post telemetry messages. 55 | 56 | - `varName:1234|g` adds or update the `varName` variable value on Teleplot *plots*. 57 | - `varName:1627551892437:1234|g` does the same but specifies the value's timestamp in milliseconds for more accurate ploting. 58 | - `varName:1627551892444:1;1627551892555:2;1627551892666:3|g` does the same as above but publishes multiple values in a single packet. 59 | 60 | > For more details on the format, check the original [Teleplot repository](https://github.com/nesnes/teleplot) 61 | 62 | ### Bash 63 | ```bash 64 | echo "myValue:1234|g" | nc -u -w0 127.0.0.1 47269 65 | ``` 66 | 67 | ### C++ 68 | Grab `clients/cpp/Teleplot.h` from the [Teleplot repository](https://github.com/nesnes/teleplot). 69 | 70 | ```c++ 71 | #include 72 | #include "Teleplot.h" 73 | Teleplot teleplot("127.0.0.1"); 74 | 75 | int main(int argc, char* argv[]) 76 | { 77 | for(float i=0;i<1000;i+=0.1) 78 | { 79 | // Use instanciated object 80 | teleplot.update("sin", sin(i)); 81 | teleplot.update("cos", cos(i), 10); // Limit at 10Hz 82 | 83 | // Use static localhost object 84 | Teleplot::localhost().update("tan", tan(i)); 85 | 86 | usleep(10000); 87 | } 88 | return 0; 89 | } 90 | ``` 91 | 92 | ## Python 93 | 94 | ```python 95 | import socket 96 | import math 97 | import time 98 | 99 | teleplotAddr = ("127.0.0.1",47269) 100 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 101 | 102 | def sendTelemetry(name, value): 103 | now = time.time() * 1000 104 | msg = name+":"+str(now)+":"+str(value)+"|g" 105 | sock.sendto(msg.encode(), teleplotAddr) 106 | 107 | i=0 108 | while i < 1000: 109 | 110 | sendTelemetry("sin", math.sin(i)) 111 | sendTelemetry("cos", math.cos(i)) 112 | 113 | i+=0.1 114 | time.sleep(0.01) 115 | ``` 116 | 117 | ## Not listed? 118 | 119 | You just need to send a UDP packet with the proper text in it. Open your web browser, search for `my_language send UDP packet`, and copy-paste the first sample you find before editing it with the following options: 120 | 121 | - address: `127.0.0.1` 122 | - port: `47269` 123 | - your test message: `myValue:1234|g` 124 | -------------------------------------------------------------------------------- /media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesnes/teleplot-vscode/c51d9086e21503669bae0d0ceaf61d84a92680df/media/favicon.ico -------------------------------------------------------------------------------- /media/images/logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesnes/teleplot-vscode/c51d9086e21503669bae0d0ceaf61d84a92680df/media/images/logo-color.png -------------------------------------------------------------------------------- /media/images/logo-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 41 | 46 | 49 | 53 | 60 | 65 | 70 | 76 | 82 | 89 | 93 | 97 | 101 | 105 | 109 | 113 | 117 | 121 | 122 | 128 | 133 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /media/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Telemetry 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | {{conn.name}} 21 | 22 |
23 | 🞢 New Serial 24 |
25 |
26 |
27 |
28 |
29 | {{input.name}} 30 | 31 |
32 |
33 |
{{input.address}}:{{input.port}}
34 |
Not connected
35 |
36 |
37 |
38 | 39 | 44 | {{input.port}} 45 | 46 | 62 | @{{input.baudrate}} 63 |
64 |
65 | 66 | 67 | 68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 |
76 | 77 | 78 | 79 |
80 |
81 |
82 |
83 |
84 | 88 |
89 | {{(telemRate+logRate).toFixed(0)}}/s 90 | 106 |
107 |
108 | 109 |
110 | 111 | 112 |
113 |
114 |
115 | 116 | 117 |
118 | 119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 |
127 | 128 |
129 | 130 |
131 |
132 | 133 |
135 | 🞢 Drop here for new chart 136 |
137 | 138 | 139 | 140 |
144 |
{{telem.name}}
145 |
{{telem.value.toFixed(4)}}
146 |
{{telem.value}}
147 |
148 |
Drag on chart for
multi-series
149 |
150 | 151 | 152 | 153 | 154 |
155 |
156 | 157 | Ready to plot incoming telemetry. 158 |
159 | 160 |
161 |
164 | 165 |
166 |
167 |
168 | 169 | 170 |
171 |
172 | 173 | 174 |
175 |
176 |
177 |
178 |
179 | 180 | {{serie.value.toFixed(4)}} 181 | {{serie.value}} 182 | 183 | {{serie.name}} 184 |
185 |
186 |
187 | Min 188 | {{serie.stats.min.toFixed(4)}} 189 |
190 |
191 | Max 192 | {{serie.stats.max.toFixed(4)}} 193 |
194 |
195 | Mean 196 | {{serie.stats.mean.toFixed(4)}} 197 |
198 |
199 | Median 200 | {{serie.stats.med.toFixed(4)}} 201 |
202 |
203 | Stdev 204 | {{serie.stats.stdev.toFixed(4)}} 205 |
206 |
207 |
208 |
209 |
210 | 212 | 213 | 214 |
215 |
216 | 217 |
218 |
219 |
220 | 221 | 222 | 223 |
224 | 225 |
226 |
227 | 228 |
229 | 230 |
231 | 232 | 233 |
234 | 236 | {{log.text}} 237 | 238 |
239 |
240 |
241 |
242 | 243 | 249 | 250 |
251 |
252 |
253 |
254 | 255 |
256 | 257 | 258 | 259 | 260 | 261 |
262 | 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /media/lib/uPlot.iife.min.js: -------------------------------------------------------------------------------- 1 | /*! https://github.com/leeoniya/uPlot (v1.6.17) */ 2 | var uPlot=function(){"use strict";function e(e,t,l,n){let i;l=l||0;let o=2147483647>=(n=n||t.length-1);for(;n-l>1;)i=o?l+n>>1:g((l+n)/2),e>t[i]?l=i:n=i;return e-t[l]>t[n]-e?n:l}function t(e,t,l,n){for(let i=1==n?t:l;i>=t&&l>=i;i+=n)if(null!=e[i])return i;return-1}const l=[0,0];function n(e,t,n,i){return l[0]=0>n?L(e,-n):e,l[1]=0>i?L(t,-i):t,l}function i(e,t,l,i){let o,s,r,u=v(e),a=10==l?y:M;return e==t&&(-1==u?(e*=l,t/=l):(e/=l,t*=l)),i?(o=g(a(e)),s=w(a(t)),r=n(k(l,o),k(l,s),o,s),e=r[0],t=r[1]):(o=g(a(m(e))),s=g(a(m(t))),r=n(k(l,o),k(l,s),o,s),e=R(e,r[0]),t=H(t,r[1])),[e,t]}function o(e,t,l,n){let o=i(e,t,l,n);return 0==e&&(o[0]=0),0==t&&(o[1]=0),o}const s={mode:3,pad:.1},r={pad:0,soft:null,mode:0},u={min:r,max:r};function a(e,t,l,n){return J(l)?f(e,t,l):(r.pad=l,r.soft=n?0:null,r.mode=n?3:0,f(e,t,u))}function c(e,t){return null==e?t:e}function f(e,t,l){let n=l.min,i=l.max,o=c(n.pad,0),s=c(i.pad,0),r=c(n.hard,-z),u=c(i.hard,z),a=c(n.soft,z),f=c(i.soft,-z),h=c(n.mode,0),d=c(i.mode,0),p=t-e;1e-9>p&&(p=0,0!=e&&0!=t||(p=1e-9,2==h&&a!=z&&(o=0),2==d&&f!=-z&&(s=0)));let x=p||m(t)||1e3,w=y(x),v=k(10,g(w)),M=L(R(e-x*(0==p?0==e?.1:1:o),v/10),9),S=a>e||1!=h&&(3!=h||M>a)&&(2!=h||a>M)?z:a,D=b(r,S>M&&e>=S?S:_(S,M)),E=L(H(t+x*(0==p?0==t?.1:1:s),v/10),9),T=t>f||1!=d&&(3!=d||f>E)&&(2!=d||E>f)?-z:f,P=_(u,E>T&&T>=t?T:b(T,E));return D==P&&0==D&&(P=100),[D,P]}const h=new Intl.NumberFormat(navigator.language).format,d=Math,p=d.PI,m=d.abs,g=d.floor,x=d.round,w=d.ceil,_=d.min,b=d.max,k=d.pow,v=d.sign,y=d.log10,M=d.log2,S=(e,t=1)=>d.asinh(e/t),z=1/0;function D(e){return 1+(0|y((e^e>>31)-(e>>31)))}function E(e,t){return x(e/t)*t}function T(e,t,l){return _(b(e,t),l)}function P(e){return"function"==typeof e?e:()=>e}const A=e=>e,W=(e,t)=>t,Y=()=>null,C=()=>!0,F=(e,t)=>e==t;function H(e,t){return w(e/t)*t}function R(e,t){return g(e/t)*t}function L(e,t){return x(e*(t=10**t))/t}const I=new Map;function G(e){return((""+e).split(".")[1]||"").length}function O(e,t,l,n){let i=[],o=n.map(G);for(let s=t;l>s;s++){let t=m(s),l=L(k(e,s),t);for(let e=0;n.length>e;e++){let r=n[e]*l,u=(0>r||0>s?t:0)+(o[e]>s?o[e]:0),a=L(r,u);i.push(a),I.set(a,u)}}return i}const N={},j=[],B=[null,null],V=Array.isArray;function U(e){return"string"==typeof e}function J(e){let t=!1;if(null!=e){let l=e.constructor;t=null==l||l==Object}return t}function q(e){return null!=e&&"object"==typeof e}function K(e,t=J){let l;if(V(e)){let n=e.find((e=>null!=e));if(V(n)||t(n)){l=Array(e.length);for(let n=0;e.length>n;n++)l[n]=K(e[n],t)}else l=e.slice()}else if(t(e)){l={};for(let n in e)l[n]=K(e[n],t)}else l=e;return l}function Z(e){let t=arguments;for(let l=1;t.length>l;l++){let n=t[l];for(let t in n)J(e[t])?Z(e[t],K(n[t])):e[t]=K(n[t])}return e}function $(e,t,l){for(let n,i=0,o=-1;t.length>i;i++){let s=t[i];if(s>o){for(n=s-1;n>=0&&null==e[n];)e[n--]=null;for(n=s+1;l>n&&null==e[n];)e[o=n++]=null}}}const X="undefined"==typeof queueMicrotask?e=>Promise.resolve().then(e):queueMicrotask,Q="width",ee="height",te="top",le="bottom",ne="left",ie="right",oe="#000",se="mousemove",re="mousedown",ue="mouseup",ae="mouseenter",ce="mouseleave",fe="dblclick",he="change",de="dppxchange",pe="u-off",me="u-label",ge=document,xe=window;let we,_e;function be(e,t){if(null!=t){let l=e.classList;!l.contains(t)&&l.add(t)}}function ke(e,t){let l=e.classList;l.contains(t)&&l.remove(t)}function ve(e,t,l){e.style[t]=l+"px"}function ye(e,t,l,n){let i=ge.createElement(e);return null!=t&&be(i,t),null!=l&&l.insertBefore(i,n),i}function Me(e,t){return ye("div",e,t)}const Se=new WeakMap;function ze(e,t,l,n,i){let o="translate("+t+"px,"+l+"px)";o!=Se.get(e)&&(e.style.transform=o,Se.set(e,o),0>t||0>l||t>n||l>i?be(e,pe):ke(e,pe))}const De=new WeakMap;function Ee(e,t,l){let n=t+l;n!=De.get(e)&&(De.set(e,n),e.style.background=t,e.style.borderColor=l)}const Te=new WeakMap;function Pe(e,t,l,n){let i=t+""+l;i!=Te.get(e)&&(Te.set(e,i),e.style.height=l+"px",e.style.width=t+"px",e.style.marginLeft=n?-t/2+"px":0,e.style.marginTop=n?-l/2+"px":0)}const Ae={passive:!0},We=Z({capture:!0},Ae);function Ye(e,t,l,n){t.addEventListener(e,l,n?We:Ae)}function Ce(e,t,l,n){t.removeEventListener(e,l,n?We:Ae)}!function e(){let t=devicePixelRatio;we!=t&&(we=t,_e&&Ce(he,_e,e),_e=matchMedia(`(min-resolution: ${we-.001}dppx) and (max-resolution: ${we+.001}dppx)`),Ye(he,_e,e),xe.dispatchEvent(new CustomEvent(de)))}();const Fe=["January","February","March","April","May","June","July","August","September","October","November","December"],He=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];function Re(e){return e.slice(0,3)}const Le=He.map(Re),Ie=Fe.map(Re),Ge={MMMM:Fe,MMM:Ie,WWWW:He,WWW:Le};function Oe(e){return(10>e?"0":"")+e}const Ne={YYYY:e=>e.getFullYear(),YY:e=>(e.getFullYear()+"").slice(2),MMMM:(e,t)=>t.MMMM[e.getMonth()],MMM:(e,t)=>t.MMM[e.getMonth()],MM:e=>Oe(e.getMonth()+1),M:e=>e.getMonth()+1,DD:e=>Oe(e.getDate()),D:e=>e.getDate(),WWWW:(e,t)=>t.WWWW[e.getDay()],WWW:(e,t)=>t.WWW[e.getDay()],HH:e=>Oe(e.getHours()),H:e=>e.getHours(),h:e=>{let t=e.getHours();return 0==t?12:t>12?t-12:t},AA:e=>12>e.getHours()?"AM":"PM",aa:e=>12>e.getHours()?"am":"pm",a:e=>12>e.getHours()?"a":"p",mm:e=>Oe(e.getMinutes()),m:e=>e.getMinutes(),ss:e=>Oe(e.getSeconds()),s:e=>e.getSeconds(),fff:e=>function(e){return(10>e?"00":100>e?"0":"")+e}(e.getMilliseconds())};function je(e,t){t=t||Ge;let l,n=[],i=/\{([a-z]+)\}|[^{]+/gi;for(;l=i.exec(e);)n.push("{"==l[0][0]?Ne[l[1]]:l[0]);return e=>{let l="";for(let i=0;n.length>i;i++)l+="string"==typeof n[i]?n[i]:n[i](e,t);return l}}const Be=(new Intl.DateTimeFormat).resolvedOptions().timeZone,Ve=e=>e%1==0,Ue=[1,2,2.5,5],Je=O(10,-16,0,Ue),qe=O(10,0,16,Ue),Ke=qe.filter(Ve),Ze=Je.concat(qe),$e="{YYYY}",Xe="\n"+$e,Qe="{M}/{D}",et="\n"+Qe,tt=et+"/{YY}",lt="{aa}",nt="{h}:{mm}"+lt,it="\n"+nt,ot=":{ss}",st=null;function rt(e){let t=1e3*e,l=60*t,n=60*l,i=24*n,o=30*i,s=365*i;return[(1==e?O(10,0,3,Ue).filter(Ve):O(10,-3,0,Ue)).concat([t,5*t,10*t,15*t,30*t,l,5*l,10*l,15*l,30*l,n,2*n,3*n,4*n,6*n,8*n,12*n,i,2*i,3*i,4*i,5*i,6*i,7*i,8*i,9*i,10*i,15*i,o,2*o,3*o,4*o,6*o,s,2*s,5*s,10*s,25*s,50*s,100*s]),[[s,$e,st,st,st,st,st,st,1],[28*i,"{MMM}",Xe,st,st,st,st,st,1],[i,Qe,Xe,st,st,st,st,st,1],[n,"{h}"+lt,tt,st,et,st,st,st,1],[l,nt,tt,st,et,st,st,st,1],[t,ot,tt+" "+nt,st,et+" "+nt,st,it,st,1],[e,ot+".{fff}",tt+" "+nt,st,et+" "+nt,st,it,st,1]],function(t){return(r,u,a,c,f,h)=>{let d=[],p=f>=s,m=f>=o&&s>f,w=t(a),_=L(w*e,3),b=gt(w.getFullYear(),p?0:w.getMonth(),m||p?1:w.getDate()),k=L(b*e,3);if(m||p){let l=m?f/o:0,n=p?f/s:0,i=_==k?_:L(gt(b.getFullYear()+n,b.getMonth()+l,1)*e,3),r=new Date(x(i/e)),u=r.getFullYear(),a=r.getMonth();for(let o=0;c>=i;o++){let s=gt(u+n*o,a+l*o,1),r=s-t(L(s*e,3));i=L((+s+r)*e,3),i>c||d.push(i)}}else{let o=i>f?f:i,s=k+(g(a)-g(_))+H(_-k,o);d.push(s);let p=t(s),m=p.getHours()+p.getMinutes()/l+p.getSeconds()/n,x=f/n,w=h/r.axes[u]._space;for(;s=L(s+f,1==e?0:3),c>=s;)if(x>1){let e=g(L(m+x,6))%24,l=t(s).getHours()-e;l>1&&(l=-1),s-=l*n,m=(m+x)%24,.7>L((s-d[d.length-1])/f,3)*w||d.push(s)}else d.push(s)}return d}}]}const[ut,at,ct]=rt(1),[ft,ht,dt]=rt(.001);function pt(e,t){return e.map((e=>e.map(((l,n)=>0==n||8==n||null==l?l:t(1==n||0==e[8]?l:e[1]+l)))))}function mt(e,t){return(l,n,i,o,s)=>{let r,u,a,c,f,h,d=t.find((e=>s>=e[0]))||t[t.length-1];return n.map((t=>{let l=e(t),n=l.getFullYear(),i=l.getMonth(),o=l.getDate(),s=l.getHours(),p=l.getMinutes(),m=l.getSeconds(),g=n!=r&&d[2]||i!=u&&d[3]||o!=a&&d[4]||s!=c&&d[5]||p!=f&&d[6]||m!=h&&d[7]||d[1];return r=n,u=i,a=o,c=s,f=p,h=m,g(l)}))}}function gt(e,t,l){return new Date(e,t,l)}function xt(e,t){return t(e)}function wt(e,t){return(l,n)=>t(e(n))}O(2,-53,53,[1]);const _t={show:!0,live:!0,isolate:!1,markers:{show:!0,width:2,stroke:function(e,t){let l=e.series[t];return l.width?l.stroke(e,t):l.points.width?l.points.stroke(e,t):null},fill:function(e,t){return e.series[t].fill(e,t)},dash:"solid"},idx:null,idxs:null,values:[]},bt=[0,0];function kt(e,t,l){return e=>{0==e.button&&l(e)}}function vt(e,t,l){return l}const yt={show:!0,x:!0,y:!0,lock:!1,move:function(e,t,l){return bt[0]=t,bt[1]=l,bt},points:{show:function(e,t){let l=e.cursor.points,n=Me(),i=l.size(e,t);ve(n,Q,i),ve(n,ee,i);let o=i/-2;ve(n,"marginLeft",o),ve(n,"marginTop",o);let s=l.width(e,t,i);return s&&ve(n,"borderWidth",s),n},size:function(e,t){return Ot(e.series[t].points.width,1)},width:0,stroke:function(e,t){let l=e.series[t].points;return l._stroke||l._fill},fill:function(e,t){let l=e.series[t].points;return l._fill||l._stroke}},bind:{mousedown:kt,mouseup:kt,click:kt,dblclick:kt,mousemove:vt,mouseleave:vt,mouseenter:vt},drag:{setScale:!0,x:!0,y:!1,dist:0,uni:null,_x:!1,_y:!1},focus:{prox:-1},left:-10,top:-10,idx:null,dataIdx:function(e,t,l){return l},idxs:null},Mt={show:!0,stroke:"rgba(0,0,0,0.07)",width:2,filter:W},St=Z({},Mt,{size:10}),zt='12px system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',Dt="bold "+zt,Et={show:!0,scale:"x",stroke:oe,space:50,gap:5,size:50,labelGap:0,labelSize:30,labelFont:Dt,side:2,grid:Mt,ticks:St,font:zt,rotate:0},Tt={show:!0,scale:"x",auto:!1,sorted:1,min:z,max:-z,idxs:[]};function Pt(e,t){return t.map((e=>null==e?"":h(e)))}function At(e,t,l,n,i,o,s){let r=[],u=I.get(i)||0;for(let e=l=s?l:L(H(l,i),u);n>=e;e=L(e+i,u))r.push(Object.is(e,-0)?0:e);return r}function Wt(e,t,l,n,i){const o=[],s=e.scales[e.axes[t].scale].log,r=g((10==s?y:M)(l));i=k(s,r),0>r&&(i=L(i,-r));let u=l;do{o.push(u),u=L(u+i,I.get(i)),i*s>u||(i=u)}while(n>=u);return o}function Yt(e,t,l,n,i){let o=e.scales[e.axes[t].scale].asinh,s=n>o?Wt(e,t,b(o,l),n,i):[o],r=0>n||l>0?[]:[0];return(-o>l?Wt(e,t,b(o,-n),-l,i):[o]).reverse().map((e=>-e)).concat(r,s)}const Ct=/./,Ft=/[12357]/,Ht=/[125]/,Rt=/1/;function Lt(e,t,l){let n=e.axes[l],i=n.scale,o=e.scales[i];if(3==o.distr&&2==o.log)return t;let s=e.valToPos,r=n._space,u=s(10,i),a=s(9,i)-u4==o.distr&&0==e||a.test(e)?e:null))}function It(e,t){return null==t?"":h(t)}const Gt={show:!0,scale:"y",stroke:oe,space:30,gap:5,size:50,labelGap:0,labelSize:30,labelFont:Dt,side:3,grid:Mt,ticks:St,font:zt,rotate:0};function Ot(e,t){return L((3+2*(e||1))*t,3)}function Nt(e,t){let l=e.scales[e.series[t].scale],n=e.bands&&e.bands.some((e=>e.series[0]==t));return 3==l.distr||n?l.min:0}const jt={scale:null,auto:!0,min:z,max:-z},Bt={show:!0,auto:!0,sorted:0,alpha:1,facets:[Z({},jt,{scale:"x"}),Z({},jt,{scale:"y"})]},Vt={scale:"y",auto:!0,sorted:0,show:!0,spanGaps:!1,gaps:(e,t,l,n,i)=>i,alpha:1,points:{show:function(e,t){let{scale:l,idxs:n}=e.series[0],i=e._data[0],o=e.valToPos(i[n[0]],l,!0),s=e.valToPos(i[n[1]],l,!0);return m(s-o)/(e.series[t].points.space*we)>=n[1]-n[0]},filter:null},values:null,min:z,max:-z,idxs:[],path:null,clip:null};function Ut(e,t,l){return l/10}const Jt={time:!0,auto:!0,distr:1,log:10,asinh:1,min:null,max:null,dir:1,ori:0},qt=Z({},Jt,{time:!1,ori:1}),Kt={};function Zt(e){let t=Kt[e];return t||(t={key:e,plots:[],sub(e){t.plots.push(e)},unsub(e){t.plots=t.plots.filter((t=>t!=e))},pub(e,l,n,i,o,s,r){for(let u=0;t.plots.length>u;u++)t.plots[u]!=l&&t.plots[u].pub(e,l,n,i,o,s,r)}},null!=e&&(Kt[e]=t)),t}function $t(e,t,l){const n=e.series[t],i=e.scales,o=e.bbox;let s=e._data[0],r=e._data[t],u=2==e.mode?i[n.facets[0].scale]:i[e.series[0].scale],a=2==e.mode?i[n.facets[1].scale]:i[n.scale],c=o.left,f=o.top,h=o.width,d=o.height,p=e.valToPosH,m=e.valToPosV;return 0==u.ori?l(n,s,r,u,a,p,m,c,f,h,d,nl,ol,rl,al,fl):l(n,s,r,u,a,m,p,f,c,d,h,il,sl,ul,cl,hl)}function Xt(e,t,l,n,i){return $t(e,t,((e,t,o,s,r,u,a,c,f,h,d)=>{let p=e.pxRound;const m=0==s.ori?ol:sl;let g,x;1==s.dir*(0==s.ori?1:-1)?(g=l,x=n):(g=n,x=l);let w=p(u(t[g],s,h,c)),_=p(a(o[g],r,d,f)),b=p(u(t[x],s,h,c)),k=p(a(r.max,r,d,f)),v=new Path2D(i);return m(v,b,k),m(v,w,k),m(v,w,_),v}))}function Qt(e,t,l,n,i,o){let s=null;if(e.length>0){s=new Path2D;const r=0==t?rl:ul;let u=l;for(let t=0;e.length>t;t++){let l=e[t];l[1]>l[0]&&(r(s,u,n,l[0]-u,n+o),u=l[1])}r(s,u,n,l+i-u,n+o)}return s}function el(e,t,l){let n=e[e.length-1];n&&n[0]==t?n[1]=l:e.push([t,l])}function tl(e){return 0==e?A:1==e?x:t=>E(t,e)}function ll(e){let t=0==e?nl:il,l=0==e?(e,t,l,n,i,o)=>{e.arcTo(t,l,n,i,o)}:(e,t,l,n,i,o)=>{e.arcTo(l,t,i,n,o)},n=0==e?(e,t,l,n,i)=>{e.rect(t,l,n,i)}:(e,t,l,n,i)=>{e.rect(l,t,i,n)};return(e,i,o,s,r,u=0)=>{0==u?n(e,i,o,s,r):(t(e,i+(u=Math.min(u,s/2,r/2)),o),l(e,i+s,o,i+s,o+r,u),l(e,i+s,o+r,i,o+r,u),l(e,i,o+r,i,o,u),l(e,i,o,i+s,o,u),e.closePath())}}const nl=(e,t,l)=>{e.moveTo(t,l)},il=(e,t,l)=>{e.moveTo(l,t)},ol=(e,t,l)=>{e.lineTo(t,l)},sl=(e,t,l)=>{e.lineTo(l,t)},rl=ll(0),ul=ll(1),al=(e,t,l,n,i,o)=>{e.arc(t,l,n,i,o)},cl=(e,t,l,n,i,o)=>{e.arc(l,t,n,i,o)},fl=(e,t,l,n,i,o,s)=>{e.bezierCurveTo(t,l,n,i,o,s)},hl=(e,t,l,n,i,o,s)=>{e.bezierCurveTo(l,t,i,n,s,o)};function dl(){return(e,t,l,n,i)=>$t(e,t,((t,o,s,r,u,a,c,f,h,d,m)=>{let g,x,{pxRound:w,points:_}=t;0==r.ori?(g=nl,x=al):(g=il,x=cl);const b=L(_.width*we,3);let k=(_.size-_.width)/2*we,v=L(2*k,3),y=new Path2D,M=new Path2D,{left:S,top:z,width:D,height:E}=e.bbox;rl(M,S-v,z-v,D+2*v,E+2*v);const T=e=>{if(null!=s[e]){let t=w(a(o[e],r,d,f)),l=w(c(s[e],u,m,h));g(y,t+k,l),x(y,t,l,k,0,2*p)}};if(i)i.forEach(T);else for(let e=l;n>=e;e++)T(e);return{stroke:b>0?y:null,fill:y,clip:M,flags:3}}))}function pl(e){return(t,l,n,i,o,s)=>{n!=i&&(o!=n&&s!=n&&e(t,l,n),o!=i&&s!=i&&e(t,l,i),e(t,l,s))}}const ml=pl(ol),gl=pl(sl);function xl(){return(e,l,n,i)=>$t(e,l,((o,s,r,u,a,c,f,h,d,p,m)=>{let g,x,w=o.pxRound;0==u.ori?(g=ol,x=ml):(g=sl,x=gl);const k=u.dir*(0==u.ori?1:-1),v={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},y=v.stroke;let M,S,D,E,T=z,P=-z,A=[],W=w(c(s[1==k?n:i],u,p,h)),Y=!1,C=!1,F=t(r,n,i,1*k),H=t(r,n,i,-1*k),R=w(c(s[F],u,p,h)),L=w(c(s[H],u,p,h));R>h&&el(A,h,R);for(let e=1==k?n:i;e>=n&&i>=e;e+=k){let t=w(c(s[e],u,p,h));if(t==W)null!=r[e]?(S=w(f(r[e],a,m,d)),T==z&&(g(y,t,S),M=S),T=_(S,T),P=b(S,P)):null===r[e]&&(Y=C=!0);else{let l=!1;T!=z?(x(y,W,T,P,M,S),D=E=W):Y&&(l=!0,Y=!1),null!=r[e]?(S=w(f(r[e],a,m,d)),g(y,t,S),T=P=M=S,C&&t-W>1&&(l=!0),C=!1):(T=z,P=-z,null===r[e]&&(Y=!0,t-W>1&&(l=!0))),l&&el(A,D,t),W=t}}if(T!=z&&T!=P&&E!=W&&x(y,W,T,P,M,S),h+p>L&&el(A,L,h+p),null!=o.fill){let t=v.fill=new Path2D(y),n=w(f(o.fillTo(e,l,o.min,o.max),a,m,d));g(t,L,n),g(t,R,n)}return v.gaps=A=o.gaps(e,l,n,i,A),o.spanGaps||(v.clip=Qt(A,u.ori,h,d,p,m)),e.bands.length>0&&(v.band=Xt(e,l,n,i,y)),v}))}function wl(e,t,l,n,i){const o=e.length;if(2>o)return null;const s=new Path2D;if(l(s,e[0],t[0]),2==o)n(s,e[1],t[1]);else{let l=Array(o),n=Array(o-1),r=Array(o-1),u=Array(o-1);for(let l=0;o-1>l;l++)r[l]=t[l+1]-t[l],u[l]=e[l+1]-e[l],n[l]=r[l]/u[l];l[0]=n[0];for(let e=1;o-1>e;e++)0===n[e]||0===n[e-1]||n[e-1]>0!=n[e]>0?l[e]=0:(l[e]=3*(u[e-1]+u[e])/((2*u[e]+u[e-1])/n[e-1]+(u[e]+2*u[e-1])/n[e]),isFinite(l[e])||(l[e]=0));l[o-1]=n[o-2];for(let n=0;o-1>n;n++)i(s,e[n]+u[n]/3,t[n]+l[n]*u[n]/3,e[n+1]-u[n]/3,t[n+1]-l[n+1]*u[n]/3,e[n+1],t[n+1])}return s}const _l=new Set;function bl(){_l.forEach((e=>{e.syncRect(!0)}))}Ye("resize",xe,bl),Ye("scroll",xe,bl,!0);const kl=xl(),vl=dl();function yl(e,t,l,n){return(n?[e[0],e[1]].concat(e.slice(2)):[e[0]].concat(e.slice(1))).map(((e,n)=>Ml(e,n,t,l)))}function Ml(e,t,l,n){return Z({},0==t?l:n,e)}function Sl(e,t,l){return null==t?B:[t,l]}const zl=Sl;function Dl(e,t,l){return null==t?B:a(t,l,.1,!0)}function El(e,t,l,n){return null==t?B:i(t,l,e.scales[n].log,!1)}const Tl=El;function Pl(e,t,l,n){return null==t?B:o(t,l,e.scales[n].log,!1)}const Al=Pl;function Wl(t,l,n,i,o){let s=b(D(t),D(l)),r=l-t,u=e(o/i*r,n);do{let e=n[u],t=i*e/r;if(t>=o&&17>=s+(5>e?I.get(e):0))return[e,t]}while(++u(t=x((l=+n)*we))+"px")),t,l]}function Cl(e){e.show&&[e.font,e.labelFont].forEach((e=>{let t=L(e[2]*we,1);e[0]=e[0].replace(/[0-9.]+px/,t+"px"),e[1]=t}))}function Fl(t,l,n){const r={mode:c(t.mode,1)},u=r.mode;function f(e,t){return((3==t.distr?y(e>0?e:t.clamp(r,e,t.min,t.max,t.key)):4==t.distr?S(e,t.asinh):e)-t._min)/(t._max-t._min)}function h(e,t,l,n){let i=f(e,t);return n+l*(-1==t.dir?1-i:i)}function g(e,t,l,n){let i=f(e,t);return n+l*(-1==t.dir?i:1-i)}function v(e,t,l,n){return 0==t.ori?h(e,t,l,n):g(e,t,l,n)}r.valToPosH=h,r.valToPosV=g;let M=!1;r.status=0;const D=r.root=Me("uplot");null!=t.id&&(D.id=t.id),be(D,t.class),t.title&&(Me("u-title",D).textContent=t.title);const A=ye("canvas"),R=r.ctx=A.getContext("2d"),I=Me("u-wrap",D),G=r.under=Me("u-under",I);I.appendChild(A);const O=r.over=Me("u-over",I),$=+c((t=K(t)).pxAlign,1),oe=tl($);(t.plugins||[]).forEach((e=>{e.opts&&(t=e.opts(r,t)||t)}));const he=t.ms||.001,_e=r.series=1==u?yl(t.series||[],Tt,Vt,!1):function(e,t){return e.map(((e,l)=>0==l?null:Z({},t,e)))}(t.series||[null],Bt),Se=r.axes=yl(t.axes||[],Et,Gt,!0),De=r.scales={},Te=r.bands=t.bands||[];Te.forEach((e=>{e.fill=P(e.fill||null)}));const Ae=2==u?_e[1].facets[0].scale:_e[0].scale,We={axes:function(){for(let e=0;Se.length>e;e++){let t=Se[e];if(!t.show||!t._show)continue;let l,n,i=t.side,o=i%2,s=t.stroke(r,e),u=0==i||3==i?-1:1;if(t.label){let e=x((t._lpos+t.labelGap*u)*we);ql(t.labelFont[0],s,"center",2==i?te:le),R.save(),1==o?(l=n=0,R.translate(e,x(Rt+Kt/2)),R.rotate((3==i?-p:p)/2)):(l=x(Ht+jt/2),n=e),R.fillText(t.label,l,n),R.restore()}let[a,c]=t._found;if(0==c)continue;let f=De[t.scale],h=0==o?jt:Kt,d=0==o?Ht:Rt,m=x(t.gap*we),g=t._splits,w=2==f.distr?g.map((e=>jl[e])):g,_=2==f.distr?jl[g[1]]-jl[g[0]]:a,b=t.ticks,k=b.show?x(b.size*we):0,y=t._rotate*-p/180,M=oe(t._pos*we),S=M+(k+m)*u;n=0==o?S:0,l=1==o?S:0,ql(t.font[0],s,1==t.align?ne:2==t.align?ie:y>0?ne:0>y?ie:0==o?"center":3==i?ie:ne,y||1==o?"middle":2==i?te:le);let z=1.5*t.font[1],D=g.map((e=>oe(v(e,f,h,d)))),E=t._values;for(let e=0;E.length>e;e++){let t=E[e];if(null!=t){0==o?l=D[e]:n=D[e],t=""+t;let i=-1==t.indexOf("\n")?[t]:t.split(/\n/gm);for(let e=0;i.length>e;e++){let t=i[e];y?(R.save(),R.translate(l,n+e*z),R.rotate(y),R.fillText(t,0,0),R.restore()):R.fillText(t,l,n+e*z)}}}b.show&&tn(D,b.filter(r,w,e,c,_),o,i,M,k,L(b.width*we,3),b.stroke(r,e),b.dash,b.cap);let T=t.grid;T.show&&tn(D,T.filter(r,w,e,c,_),o,0==o?2:1,0==o?Rt:Ht,0==o?Kt:jt,L(T.width*we,3),T.stroke(r,e),T.dash,T.cap)}ti("drawAxes")},series:function(){pl>0&&(_e.forEach(((e,t)=>{if(t>0&&e.show&&null==e._paths){let n=function(e){let t=T(ml-1,0,pl-1),l=T(gl+1,0,pl-1);for(;null==e[t]&&t>0;)t--;for(;null==e[l]&&pl-1>l;)l++;return[t,l]}(l[t]);e._paths=e.paths(r,t,n[0],n[1])}})),_e.forEach(((e,t)=>{if(t>0&&e.show){Nl!=e.alpha&&(R.globalAlpha=Nl=e.alpha),Zl(t,!1),e._paths&&$l(t,!1);{Zl(t,!0);let l=e.points.show(r,t,ml,gl),n=e.points.filter(r,t,l,e._paths?e._paths.gaps:null);(l||n)&&(e.points._paths=e.points.paths(r,t,ml,gl,n),$l(t,!0))}1!=Nl&&(R.globalAlpha=Nl=1),ti("drawSeries",t)}})))}},Fe=(t.drawOrder||["axes","series"]).map((e=>We[e]));function He(e){let l=De[e];if(null==l){let n=(t.scales||N)[e]||N;if(null!=n.from)He(n.from),De[e]=Z({},De[n.from],n,{key:e});else{l=De[e]=Z({},e==Ae?Jt:qt,n),2==u&&(l.time=!1),l.key=e;let t=l.time,i=l.range,o=V(i);if((e!=Ae||2==u)&&(!o||null!=i[0]&&null!=i[1]||(i={min:null==i[0]?s:{mode:1,hard:i[0],soft:i[0]},max:null==i[1]?s:{mode:1,hard:i[1],soft:i[1]}},o=!1),!o&&J(i))){let e=i;i=(t,l,n)=>null==l?B:a(l,n,e)}l.range=P(i||(t?zl:e==Ae?3==l.distr?Tl:4==l.distr?Al:Sl:3==l.distr?El:4==l.distr?Pl:Dl)),l.auto=P(!o&&l.auto),l.clamp=P(l.clamp||Ut),l._min=l._max=null}}}He("x"),He("y"),1==u&&_e.forEach((e=>{He(e.scale)})),Se.forEach((e=>{He(e.scale)}));for(let e in t.scales)He(e);const Re=De[Ae],Le=Re.distr;let Ie,Ge;0==Re.ori?(be(D,"u-hz"),Ie=h,Ge=g):(be(D,"u-vt"),Ie=g,Ge=h);const Oe={};for(let e in De){let t=De[e];null==t.min&&null==t.max||(Oe[e]={min:t.min,max:t.max},t.min=t.max=null)}const Ne=t.tzDate||(e=>new Date(x(e/he))),Be=t.fmtDate||je,Ve=1==he?ct(Ne):dt(Ne),Ue=mt(Ne,pt(1==he?at:ht,Be)),Je=wt(Ne,xt("{YYYY}-{MM}-{DD} {h}:{mm}{aa}",Be)),qe=[],$e=r.legend=Z({},_t,t.legend),Xe=$e.show,Qe=$e.markers;let et;$e.idxs=qe,Qe.width=P(Qe.width),Qe.dash=P(Qe.dash),Qe.stroke=P(Qe.stroke),Qe.fill=P(Qe.fill);let tt,lt=[],nt=[],it=!1,ot={};if($e.live){const e=_e[1]?_e[1].values:null;it=null!=e,tt=it?e(r,1,0):{_:0};for(let e in tt)ot[e]="--"}if(Xe)if(et=ye("table","u-legend",D),it){let e=ye("tr","u-thead",et);for(var st in ye("th",null,e),tt)ye("th",me,e).textContent=st}else be(et,"u-inline"),$e.live&&be(et,"u-live");const rt={show:!0},gt={show:!1},bt=new Map;function kt(e,t,l){const n=bt.get(t)||{},i=ol.bind[e](r,t,l);i&&(Ye(e,t,n[e]=i),bt.set(t,n))}function vt(e,t){const l=bt.get(t)||{};for(let n in l)null!=e&&n!=e||(Ce(n,t,l[n]),delete l[n]);null==e&&bt.delete(t)}let Mt=0,St=0,zt=0,Dt=0,Ct=0,Ft=0,Ht=0,Rt=0,jt=0,Kt=0;r.bbox={};let $t=!1,Xt=!1,Qt=!1,el=!1,ll=!1;function nl(e,t,l){(l||e!=r.width||t!=r.height)&&il(e,t),on(!1),Qt=!0,Xt=!0,el=ll=ol.left>=0,_n()}function il(e,t){r.width=Mt=zt=e,r.height=St=Dt=t,Ct=Ft=0,function(){let e=!1,t=!1,l=!1,n=!1;Se.forEach((i=>{if(i.show&&i._show){let{side:o,_size:s}=i,r=o%2,u=s+(null!=i.label?i.labelSize:0);u>0&&(r?(zt-=u,3==o?(Ct+=u,n=!0):l=!0):(Dt-=u,0==o?(Ft+=u,e=!0):t=!0))}})),cl[0]=e,cl[1]=l,cl[2]=t,cl[3]=n,zt-=dl[1]+dl[3],Ct+=dl[3],Dt-=dl[2]+dl[0],Ft+=dl[0]}(),function(){let e=Ct+zt,t=Ft+Dt,l=Ct,n=Ft;function i(i,o){switch(i){case 1:return e+=o,e-o;case 2:return t+=o,t-o;case 3:return l-=o,l+o;case 0:return n-=o,n+o}}Se.forEach((e=>{if(e.show&&e._show){let t=e.side;e._pos=i(t,e._size),null!=e.label&&(e._lpos=i(t,e.labelSize))}}))}();let l=r.bbox;Ht=l.left=E(Ct*we,.5),Rt=l.top=E(Ft*we,.5),jt=l.width=E(zt*we,.5),Kt=l.height=E(Dt*we,.5)}r.setSize=function({width:e,height:t}){nl(e,t)};const ol=r.cursor=Z({},yt,{drag:{y:2==u}},t.cursor);{ol.idxs=qe,ol._lock=!1;let e=ol.points;e.show=P(e.show),e.size=P(e.size),e.stroke=P(e.stroke),e.width=P(e.width),e.fill=P(e.fill)}const sl=r.focus=Z({},t.focus||{alpha:.3},ol.focus),rl=sl.prox>=0;let ul=[null];function al(e,t){if(1==u||t>0){let t=1==u&&De[e.scale].time,l=e.value;e.value=t?U(l)?wt(Ne,xt(l,Be)):l||Je:l||It,e.label=e.label||(t?"Time":"Value")}if(t>0){e.width=null==e.width?1:e.width,e.paths=e.paths||kl||Y,e.fillTo=P(e.fillTo||Nt),e.pxAlign=+c(e.pxAlign,$),e.pxRound=tl(e.pxAlign),e.stroke=P(e.stroke||null),e.fill=P(e.fill||null),e._stroke=e._fill=e._paths=e._focus=null;let t=Ot(e.width,1),l=e.points=Z({},{size:t,width:b(1,.2*t),stroke:e.stroke,space:2*t,paths:vl,_stroke:null,_fill:null},e.points);l.show=P(l.show),l.filter=P(l.filter),l.fill=P(l.fill),l.stroke=P(l.stroke),l.paths=P(l.paths),l.pxAlign=e.pxAlign}if(Xe){let l=function(e,t){if(0==t&&(it||!$e.live||2==u))return B;let l=[],n=ye("tr","u-series",et,et.childNodes[t]);be(n,e.class),e.show||be(n,pe);let i=ye("th",null,n);if(Qe.show){let e=Me("u-marker",i);if(t>0){let l=Qe.width(r,t);l&&(e.style.border=l+"px "+Qe.dash(r,t)+" "+Qe.stroke(r,t)),e.style.background=Qe.fill(r,t)}}let o=Me(me,i);for(var s in o.textContent=e.label,t>0&&(Qe.show||(o.style.color=e.width>0?Qe.stroke(r,t):Qe.fill(r,t)),kt("click",i,(t=>{if(ol._lock)return;let l=_e.indexOf(e);if((t.ctrlKey||t.metaKey)!=$e.isolate){let e=_e.some(((e,t)=>t>0&&t!=l&&e.show));_e.forEach(((t,n)=>{n>0&&Pn(n,e?n==l?rt:gt:rt,!0,li.setSeries)}))}else Pn(l,{show:!e.show},!0,li.setSeries)})),rl&&kt(ae,i,(()=>{ol._lock||Pn(_e.indexOf(e),Cn,!0,li.setSeries)}))),tt){let e=ye("td","u-value",n);e.textContent="--",l.push(e)}return[n,l]}(e,t);lt.splice(t,0,l[0]),nt.splice(t,0,l[1]),$e.values.push(null)}if(ol.show){qe.splice(t,0,null);let l=function(e,t){if(t>0){let l=ol.points.show(r,t);if(l)return be(l,"u-cursor-pt"),be(l,e.class),ze(l,-10,-10,zt,Dt),O.insertBefore(l,ul[t]),l}}(e,t);l&&ul.splice(t,0,l)}}r.addSeries=function(e,t){e=Ml(e,t=null==t?_e.length:t,Tt,Vt),_e.splice(t,0,e),al(_e[t],t)},r.delSeries=function(e){if(_e.splice(e,1),Xe){$e.values.splice(e,1),nt.splice(e,1);let t=lt.splice(e,1)[0];vt(null,t.firstChild),t.remove()}ol.show&&(qe.splice(e,1),ul.length>1&&ul.splice(e,1)[0].remove())};const cl=[!1,!1,!1,!1];function fl(e,t,l){let[n,i,o,s]=l,r=t%2,u=0;return 0==r&&(s||i)&&(u=0==t&&!n||2==t&&!o?x(Et.size/3):0),1==r&&(n||o)&&(u=1==t&&!i||3==t&&!s?x(Gt.size/2):0),u}const hl=r.padding=(t.padding||[fl,fl,fl,fl]).map((e=>P(c(e,fl)))),dl=r._padding=hl.map(((e,t)=>e(r,t,cl,0)));let pl,ml=null,gl=null;const xl=1==u?_e[0].idxs:null;let wl,bl,Fl,Hl,Rl,Ll,Il,Gl,Ol,Nl,jl=null,Bl=!1;function Vl(e,t){if(2==u){pl=0;for(let e=1;_e.length>e;e++)pl+=l[e][0].length;r.data=l=e}else(l=(e||[]).slice())[0]=l[0]||[],r.data=l.slice(),jl=l[0],pl=jl.length,2==Le&&(l[0]=jl.map(((e,t)=>t)));if(r._data=l,on(!0),ti("setData"),!1!==t){let e=Re;e.auto(r,Bl)?Ul():Tn(Ae,e.min,e.max),el=ol.left>=0,ll=!0,_n()}}function Ul(){let e,t;Bl=!0,1==u&&(pl>0?(ml=xl[0]=0,gl=xl[1]=pl-1,e=l[0][ml],t=l[0][gl],2==Le?(e=ml,t=gl):1==pl&&(3==Le?[e,t]=i(e,e,Re.log,!1):4==Le?[e,t]=o(e,e,Re.log,!1):Re.time?t=e+x(86400/he):[e,t]=a(e,t,.1,!0))):(ml=xl[0]=e=null,gl=xl[1]=t=null)),Tn(Ae,e,t)}function Jl(e="#0000",t,l=j,n="butt",i="#0000",o="round"){e!=wl&&(R.strokeStyle=wl=e),i!=bl&&(R.fillStyle=bl=i),t!=Fl&&(R.lineWidth=Fl=t),o!=Rl&&(R.lineJoin=Rl=o),n!=Ll&&(R.lineCap=Ll=n),l!=Hl&&R.setLineDash(Hl=l)}function ql(e,t,l,n){t!=bl&&(R.fillStyle=bl=t),e!=Il&&(R.font=Il=e),l!=Gl&&(R.textAlign=Gl=l),n!=Ol&&(R.textBaseline=Ol=n)}function Kl(e,t,l,n){if(e.auto(r,Bl)&&(null==t||null==t.min)){let t=c(ml,0),i=c(gl,n.length-1),o=null==l.min?3==e.distr?function(e,t,l){let n=z,i=-z;for(let o=t;l>=o;o++)e[o]>0&&(n=_(n,e[o]),i=b(i,e[o]));return[n==z?1:n,i==-z?10:i]}(n,t,i):function(e,t,l){let n=z,i=-z;for(let o=t;l>=o;o++)null!=e[o]&&(n=_(n,e[o]),i=b(i,e[o]));return[n,i]}(n,t,i):[l.min,l.max];e.min=_(e.min,l.min=o[0]),e.max=b(e.max,l.max=o[1])}}function Zl(e,t){let l=t?_e[e].points:_e[e];l._stroke=l.stroke(r,e),l._fill=l.fill(r,e)}function $l(e,t){let n=t?_e[e].points:_e[e],i=n._stroke,o=n._fill,{stroke:s,fill:u,clip:a,flags:f}=n._paths,h=null,d=L(n.width*we,3),p=d%2/2;t&&null==o&&(o=d>0?"#fff":i);let m=1==n.pxAlign;if(m&&R.translate(p,p),!t){let e=Ht,t=Rt,l=jt,i=Kt,o=d*we/2;0==n.min&&(i+=o),0==n.max&&(t-=o,i+=o),h=new Path2D,h.rect(e,t,l,i)}t?Xl(i,d,n.dash,n.cap,o,s,u,f,a):function(e,t,n,i,o,s,u,a,f,h,d){let p=!1;Te.forEach(((m,g)=>{if(m.series[0]==e){let e,x=_e[m.series[1]],w=l[m.series[1]],_=(x._paths||N).band,b=null;x.show&&_&&function(e,t,l){for(t=c(t,0),l=c(l,e.length-1);l>=t;){if(null!=e[t])return!0;t++}return!1}(w,ml,gl)?(b=m.fill(r,g)||s,e=x._paths.clip):_=null,Xl(t,n,i,o,b,u,a,f,h,d,e,_),p=!0}})),p||Xl(t,n,i,o,s,u,a,f,h,d)}(e,i,d,n.dash,n.cap,o,s,u,f,h,a),m&&R.translate(-p,-p)}function Xl(e,t,l,n,i,o,s,r,u,a,c,f){Jl(e,t,l,n,i),(u||a||f)&&(R.save(),u&&R.clip(u),a&&R.clip(a)),f?3==(3&r)?(R.clip(f),c&&R.clip(c),en(i,s),Ql(e,o,t)):2&r?(en(i,s),R.clip(f),Ql(e,o,t)):1&r&&(R.save(),R.clip(f),c&&R.clip(c),en(i,s),R.restore(),Ql(e,o,t)):(en(i,s),Ql(e,o,t)),(u||a||f)&&R.restore()}function Ql(e,t,l){l>0&&(t instanceof Map?t.forEach(((e,t)=>{R.strokeStyle=wl=t,R.stroke(e)})):null!=t&&e&&R.stroke(t))}function en(e,t){t instanceof Map?t.forEach(((e,t)=>{R.fillStyle=bl=t,R.fill(e)})):null!=t&&e&&R.fill(t)}function tn(e,t,l,n,i,o,s,r,u,a){let c=s%2/2;1==$&&R.translate(c,c),Jl(r,s,u,a,r),R.beginPath();let f,h,d,p,m=i+(0==n||3==n?-o:o);0==l?(h=i,p=m):(f=i,d=m);for(let n=0;e.length>n;n++)null!=t[n]&&(0==l?f=d=e[n]:h=p=e[n],R.moveTo(f,h),R.lineTo(d,p));R.stroke(),1==$&&R.translate(-c,-c)}function ln(e){let t=!0;return Se.forEach(((l,n)=>{if(!l.show)return;let i=De[l.scale];if(null==i.min)return void(l._show&&(t=!1,l._show=!1,on(!1)));l._show||(t=!1,l._show=!0,on(!1));let o=l.side,s=o%2,{min:u,max:a}=i,[c,f]=function(e,t,l,n){let i,o=Se[e];if(n>0){let s=o._space=o.space(r,e,t,l,n);i=Wl(t,l,o._incrs=o.incrs(r,e,t,l,n,s),n,s)}else i=[0,0];return o._found=i}(n,u,a,0==s?zt:Dt);if(0==f)return;let h=l._splits=l.splits(r,n,u,a,c,f,2==i.distr),d=2==i.distr?h.map((e=>jl[e])):h,p=2==i.distr?jl[h[1]]-jl[h[0]]:c,m=l._values=l.values(r,l.filter(r,d,n,f,p),n,f,p);l._rotate=2==o?l.rotate(r,m,n,f):0;let g=l._size;l._size=w(l.size(r,m,n,e)),null!=g&&l._size!=g&&(t=!1)})),t}function nn(e){let t=!0;return hl.forEach(((l,n)=>{let i=l(r,n,cl,e);i!=dl[n]&&(t=!1),dl[n]=i})),t}function on(e){_e.forEach(((t,l)=>{l>0&&(t._paths=null,e&&(1==u?(t.min=null,t.max=null):t.facets.forEach((e=>{e.min=null,e.max=null}))))}))}r.setData=Vl;let sn,rn,un,an,cn,fn,hn,dn,pn,mn,gn,xn,wn=!1;function _n(){wn||(X(bn),wn=!0)}function bn(){$t&&(function(){let t=K(De,q);for(let e in t){let l=t[e],n=Oe[e];if(null!=n&&null!=n.min)Z(l,n),e==Ae&&on(!0);else if(e!=Ae||2==u)if(0==pl&&null==l.from){let t=l.range(r,null,null,e);l.min=t[0],l.max=t[1]}else l.min=z,l.max=-z}if(pl>0){_e.forEach(((n,i)=>{if(1==u){let o=n.scale,s=t[o],u=Oe[o];if(0==i){let t=s.range(r,s.min,s.max,o);s.min=t[0],s.max=t[1],ml=e(s.min,l[0]),gl=e(s.max,l[0]),s.min>l[0][ml]&&ml++,l[0][gl]>s.max&&gl--,n.min=jl[ml],n.max=jl[gl]}else n.show&&n.auto&&Kl(s,u,n,l[i]);n.idxs[0]=ml,n.idxs[1]=gl}else if(i>0&&n.show&&n.auto){let[e,o]=n.facets,s=e.scale,r=o.scale,[u,a]=l[i];Kl(t[s],Oe[s],e,u),Kl(t[r],Oe[r],o,a),n.min=o.min,n.max=o.max}}));for(let e in t){let l=t[e],n=Oe[e];if(null==l.from&&(null==n||null==n.min)){let t=l.range(r,l.min==z?null:l.min,l.max==-z?null:l.max,e);l.min=t[0],l.max=t[1]}}}for(let e in t){let l=t[e];if(null!=l.from){let n=t[l.from];if(null==n.min)l.min=l.max=null;else{let t=l.range(r,n.min,n.max,e);l.min=t[0],l.max=t[1]}}}let n={},i=!1;for(let e in t){let l=t[e],o=De[e];if(o.min!=l.min||o.max!=l.max){o.min=l.min,o.max=l.max;let t=o.distr;o._min=3==t?y(o.min):4==t?S(o.min,o.asinh):o.min,o._max=3==t?y(o.max):4==t?S(o.max,o.asinh):o.max,n[e]=i=!0}}if(i){_e.forEach(((e,t)=>{2==u?t>0&&n.y&&(e._paths=null):n[e.scale]&&(e._paths=null)}));for(let e in n)Qt=!0,ti("setScale",e);ol.show&&(el=ll=ol.left>=0)}for(let e in Oe)Oe[e]=null}(),$t=!1),Qt&&(function(){let e=!1,t=0;for(;!e;){t++;let l=ln(t),n=nn(t);e=3==t||l&&n,e||(il(r.width,r.height),Xt=!0)}}(),Qt=!1),Xt&&(ve(G,ne,Ct),ve(G,te,Ft),ve(G,Q,zt),ve(G,ee,Dt),ve(O,ne,Ct),ve(O,te,Ft),ve(O,Q,zt),ve(O,ee,Dt),ve(I,Q,Mt),ve(I,ee,St),A.width=x(Mt*we),A.height=x(St*we),Se.forEach((e=>{let{_show:t,_el:l,_size:n,_pos:i,side:o}=e;if(t){let e=o%2==1;ve(l,e?"left":"top",i-(3===o||0===o?n:0)),ve(l,e?"width":"height",n),ve(l,e?"top":"left",e?Ft:Ct),ve(l,e?"height":"width",e?Dt:zt),l&&ke(l,pe)}else l&&be(l,pe)})),wl=bl=Fl=Rl=Ll=Il=Gl=Ol=Hl=null,Nl=1,Vn(!1),ti("setSize"),Xt=!1),Mt>0&&St>0&&(R.clearRect(0,0,A.width,A.height),ti("drawClear"),Fe.forEach((e=>e())),ti("draw")),ol.show&&el&&(jn(null,!0,!1),el=!1),M||(M=!0,r.status=1,ti("ready")),Bl=!1,wn=!1}function kn(t,n){let i=De[t];if(null==i.from){if(0==pl){let e=i.range(r,n.min,n.max,t);n.min=e[0],n.max=e[1]}if(n.min>n.max){let e=n.min;n.min=n.max,n.max=e}if(pl>1&&null!=n.min&&null!=n.max&&1e-16>n.max-n.min)return;t==Ae&&2==i.distr&&pl>0&&(n.min=e(n.min,l[0]),n.max=e(n.max,l[0])),Oe[t]=n,$t=!0,_n()}}r.redraw=(e,t)=>{Qt=t||!1,!1!==e?Tn(Ae,Re.min,Re.max):_n()},r.setScale=kn;let vn=!1;const yn=ol.drag;let Mn=yn.x,Sn=yn.y;ol.show&&(ol.x&&(sn=Me("u-cursor-x",O)),ol.y&&(rn=Me("u-cursor-y",O)),0==Re.ori?(un=sn,an=rn):(un=rn,an=sn),gn=ol.left,xn=ol.top);const zn=r.select=Z({show:!0,over:!0,left:0,width:0,top:0,height:0},t.select),Dn=zn.show?Me("u-select",zn.over?O:G):null;function En(e,t){if(zn.show){for(let t in e)ve(Dn,t,zn[t]=e[t]);!1!==t&&ti("setSelect")}}function Tn(e,t,l){kn(e,{min:t,max:l})}function Pn(e,t,l,n){let i=_e[e];null!=t.focus&&function(e){if(e!=Yn){let t=null==e,l=1!=sl.alpha;_e.forEach(((n,i)=>{let o=t||0==i||i==e;n._focus=t?null:o,l&&function(e,t){_e[e].alpha=t,ol.show&&ul[e]&&(ul[e].style.opacity=t),Xe&<[e]&&(lt[e].style.opacity=t)}(i,o?1:sl.alpha)})),Yn=e,l&&_n()}}(e),null!=t.show&&(i.show=t.show,function(e){let t=Xe?lt[e]:null;_e[e].show?t&&ke(t,pe):(t&&be(t,pe),ul.length>1&&ze(ul[e],-10,-10,zt,Dt))}(e),Tn(2==u?i.facets[1].scale:i.scale,null,null),_n()),!1!==l&&ti("setSeries",e,t),n&&oi("setSeries",r,e,t)}let An,Wn,Yn;r.setSelect=En,r.setSeries=Pn,r.addBand=function(e,t){e.fill=P(e.fill||null),Te.splice(t=null==t?Te.length:t,0,e)},r.setBand=function(e,t){Z(Te[e],t)},r.delBand=function(e){null==e?Te.length=0:Te.splice(e,1)};const Cn={focus:!0},Fn={focus:!1};function Hn(e,t,l){let n=De[t];l&&(e=e/we-(1==n.ori?Ft:Ct));let i=zt;1==n.ori&&(i=Dt,e=i-e),-1==n.dir&&(e=i-e);let o=n._min,s=o+e/i*(n._max-o),r=n.distr;return 3==r?k(10,s):4==r?((e,t=1)=>d.sinh(e)*t)(s,n.asinh):s}function Rn(e,t){ve(Dn,ne,zn.left=e),ve(Dn,Q,zn.width=t)}function Ln(e,t){ve(Dn,te,zn.top=e),ve(Dn,ee,zn.height=t)}Xe&&rl&&Ye(ce,et,(()=>{ol._lock||(Pn(null,Fn,!0,li.setSeries),jn(null,!0,!1))})),r.valToIdx=t=>e(t,l[0]),r.posToIdx=function(t,n){return e(Hn(t,Ae,n),l[0],ml,gl)},r.posToVal=Hn,r.valToPos=(e,t,l)=>0==De[t].ori?h(e,De[t],l?jt:zt,l?Ht:0):g(e,De[t],l?Kt:Dt,l?Rt:0),r.batch=function(e){e(r),_n()},r.setCursor=(e,t,l)=>{gn=e.left,xn=e.top,jn(null,t,l)};let In=0==Re.ori?Rn:Ln,Gn=1==Re.ori?Rn:Ln;function On(e,t){if(null!=e){let t=e.idx;$e.idx=t,_e.forEach(((e,l)=>{(l>0||!it)&&Nn(l,t)}))}Xe&&$e.live&&function(){if(Xe&&$e.live)for(let e=2==u?1:0;_e.length>e;e++){if(0==e&&it)continue;let t=$e.values[e],l=0;for(let n in t)nt[e][l++].firstChild.nodeValue=t[n]}}(),ll=!1,!1!==t&&ti("setLegend")}function Nn(e,t){let n;if(null==t)n=ot;else{let i=_e[e],o=0==e&&2==Le?jl:l[e];n=it?i.values(r,e,t):{_:i.value(r,o[t],e,t)}}$e.values[e]=n}function jn(t,n,i){let o;pn=gn,mn=xn,[gn,xn]=ol.move(r,gn,xn),ol.show&&(un&&ze(un,x(gn),0,zt,Dt),an&&ze(an,0,x(xn),zt,Dt)),An=z;let s=0==Re.ori?zt:Dt,a=1==Re.ori?zt:Dt;if(0>gn||0==pl||ml>gl){o=null;for(let e=0;_e.length>e;e++)e>0&&ul.length>1&&ze(ul[e],-10,-10,zt,Dt);if(rl&&Pn(null,Cn,!0,null==t&&li.setSeries),$e.live){qe.fill(null),ll=!0;for(let e=0;_e.length>e;e++)$e.values[e]=ot}}else{let t,n,i;1==u&&(t=0==Re.ori?gn:xn,n=Hn(t,Ae),o=e(n,l[0],ml,gl),i=H(Ie(l[0][o],Re,s,0),.5));for(let e=2==u?1:0;_e.length>e;e++){let t=_e[e],c=qe[e],f=1==u?l[e][c]:l[e][1][c],h=ol.dataIdx(r,e,o,n),d=1==u?l[e][h]:l[e][1][h];ll=ll||d!=f||h!=c,qe[e]=h;let p=h==o?i:H(Ie(1==u?l[0][h]:l[e][0][h],Re,s,0),.5);if(e>0&&t.show){let l,n,i=null==d?-10:H(Ge(d,1==u?De[t.scale]:De[t.facets[1].scale],a,0),.5);if(i>0&&1==u){let t=m(i-xn);t>An||(An=t,Wn=e)}if(0==Re.ori?(l=p,n=i):(l=i,n=p),ll&&ul.length>1){Ee(ul[e],ol.points.fill(r,e),ol.points.stroke(r,e));let t,i,o,s,u=!0,a=ol.points.bbox;if(null!=a){u=!1;let l=a(r,e);o=l.left,s=l.top,t=l.width,i=l.height}else o=l,s=n,t=i=ol.points.size(r,e);Pe(ul[e],t,i,u),ze(ul[e],o,s,zt,Dt)}}if($e.live){if(!ll||0==e&&it)continue;Nn(e,h)}}}if(ol.idx=o,ol.left=gn,ol.top=xn,ll&&($e.idx=o,On()),zn.show&&vn)if(null!=t){let[e,l]=li.scales,[n,i]=li.match,[o,r]=t.cursor.sync.scales,u=t.cursor.drag;Mn=u._x,Sn=u._y;let c,f,h,d,p,{left:g,top:x,width:w,height:b}=t.select,k=t.scales[e].ori,v=t.posToVal,y=null!=e&&n(e,o),M=null!=l&&i(l,r);y&&(0==k?(c=g,f=w):(c=x,f=b),Mn?(h=De[e],d=Ie(v(c,o),h,s,0),p=Ie(v(c+f,o),h,s,0),In(_(d,p),m(p-d))):In(0,s),M||Gn(0,a)),M&&(1==k?(c=g,f=w):(c=x,f=b),Sn?(h=De[l],d=Ge(v(c,r),h,a,0),p=Ge(v(c+f,r),h,a,0),Gn(_(d,p),m(p-d))):Gn(0,a),y||In(0,s))}else{let e=m(pn-cn),t=m(mn-fn);if(1==Re.ori){let l=e;e=t,t=l}Mn=yn.x&&e>=yn.dist,Sn=yn.y&&t>=yn.dist;let l,n,i=yn.uni;null!=i?Mn&&Sn&&(Mn=e>=i,Sn=t>=i,Mn||Sn||(t>e?Sn=!0:Mn=!0)):yn.x&&yn.y&&(Mn||Sn)&&(Mn=Sn=!0),Mn&&(0==Re.ori?(l=hn,n=gn):(l=dn,n=xn),In(_(l,n),m(n-l)),Sn||Gn(0,a)),Sn&&(1==Re.ori?(l=hn,n=gn):(l=dn,n=xn),Gn(_(l,n),m(n-l)),Mn||In(0,s)),Mn||Sn||(In(0,0),Gn(0,0))}if(yn._x=Mn,yn._y=Sn,null==t){if(i){if(null!=ni){let[e,t]=li.scales;li.values[0]=null!=e?Hn(0==Re.ori?gn:xn,e):null,li.values[1]=null!=t?Hn(1==Re.ori?gn:xn,t):null}oi(se,r,gn,xn,zt,Dt,o)}if(rl){let e=i&&li.setSeries,t=sl.prox;null==Yn?An>t||Pn(Wn,Cn,!0,e):An>t?Pn(null,Cn,!0,e):Wn!=Yn&&Pn(Wn,Cn,!0,e)}}M&&!1!==n&&ti("setCursor")}r.setLegend=On;let Bn=null;function Vn(e){!0===e?Bn=null:(Bn=O.getBoundingClientRect(),ti("syncRect",Bn))}function Un(e,t,l,n,i,o){ol._lock||(Jn(e,t,l,n,i,o,0,!1,null!=e),null!=e?jn(null,!0,!0):jn(t,!0,!1))}function Jn(e,t,l,n,i,o,s,u,a){if(null==Bn&&Vn(!1),null!=e)l=e.clientX-Bn.left,n=e.clientY-Bn.top;else{if(0>l||0>n)return gn=-10,void(xn=-10);let[e,s]=li.scales,r=t.cursor.sync,[u,a]=r.values,[c,f]=r.scales,[h,d]=li.match,p=1==t.scales[c].ori,m=0==Re.ori?zt:Dt,g=1==Re.ori?zt:Dt,x=p?o:i,w=p?i:o,_=p?n:l,b=p?l:n;if(l=null!=c?h(e,c)?v(u,De[e],m,0):-10:m*(_/x),n=null!=f?d(s,f)?v(a,De[s],g,0):-10:g*(b/w),1==Re.ori){let e=l;l=n,n=e}}a&&(l>1&&zt-1>l||(l=E(l,zt)),n>1&&Dt-1>n||(n=E(n,Dt))),u?(cn=l,fn=n,[hn,dn]=ol.move(r,l,n)):(gn=l,xn=n)}function qn(){En({width:0,height:0},!1)}function Kn(e,t,l,n,i,o){vn=!0,Mn=Sn=yn._x=yn._y=!1,Jn(e,t,l,n,i,o,0,!0,!1),null!=e&&(kt(ue,ge,Zn),oi(re,r,hn,dn,zt,Dt,null))}function Zn(e,t,l,n,i,o){vn=yn._x=yn._y=!1,Jn(e,t,l,n,i,o,0,!1,!0);let{left:s,top:u,width:a,height:c}=zn,f=a>0||c>0;if(f&&En(zn),yn.setScale&&f){let e=s,t=a,l=u,n=c;if(1==Re.ori&&(e=u,t=c,l=s,n=a),Mn&&Tn(Ae,Hn(e,Ae),Hn(e+t,Ae)),Sn)for(let e in De){let t=De[e];e!=Ae&&null==t.from&&t.min!=z&&Tn(e,Hn(l+n,e),Hn(l,e))}qn()}else ol.lock&&(ol._lock=!ol._lock,ol._lock||jn(null,!0,!1));null!=e&&(vt(ue,ge),oi(ue,r,gn,xn,zt,Dt,null))}function $n(e){Ul(),qn(),null!=e&&oi(fe,r,gn,xn,zt,Dt,null)}function Xn(){Se.forEach(Cl),nl(r.width,r.height,!0)}Ye(de,xe,Xn);const Qn={};Qn.mousedown=Kn,Qn.mousemove=Un,Qn.mouseup=Zn,Qn.dblclick=$n,Qn.setSeries=(e,t,l,n)=>{Pn(l,n,!0,!1)},ol.show&&(kt(re,O,Kn),kt(se,O,Un),kt(ae,O,Vn),kt(ce,O,(function(){if(!ol._lock){let e=vn;if(vn){let e,t,l=!0,n=!0,i=10;0==Re.ori?(e=Mn,t=Sn):(e=Sn,t=Mn),e&&t&&(l=i>=gn||gn>=zt-i,n=i>=xn||xn>=Dt-i),e&&l&&(gn=hn>gn?0:zt),t&&n&&(xn=dn>xn?0:Dt),jn(null,!0,!0),vn=!1}gn=-10,xn=-10,jn(null,!0,!0),e&&(vn=e)}})),kt(fe,O,$n),_l.add(r),r.syncRect=Vn);const ei=r.hooks=t.hooks||{};function ti(e,t,l){e in ei&&ei[e].forEach((e=>{e.call(null,r,t,l)}))}(t.plugins||[]).forEach((e=>{for(let t in e.hooks)ei[t]=(ei[t]||[]).concat(e.hooks[t])}));const li=Z({key:null,setSeries:!1,filters:{pub:C,sub:C},scales:[Ae,_e[1]?_e[1].scale:null],match:[F,F],values:[null,null]},ol.sync);ol.sync=li;const ni=li.key,ii=Zt(ni);function oi(e,t,l,n,i,o,s){li.filters.pub(e,t,l,n,i,o,s)&&ii.pub(e,t,l,n,i,o,s)}function si(){ti("init",t,l),Vl(l||t.data,!1),Oe[Ae]?kn(Ae,Oe[Ae]):Ul(),nl(t.width,t.height),jn(null,!0,!1),En(zn,!1)}return ii.sub(r),r.pub=function(e,t,l,n,i,o,s){li.filters.sub(e,t,l,n,i,o,s)&&Qn[e](null,t,l,n,i,o,s)},r.destroy=function(){ii.unsub(r),_l.delete(r),bt.clear(),Ce(de,xe,Xn),D.remove(),ti("destroy")},_e.forEach(al),Se.forEach((function(e,t){if(e._show=e.show,e.show){let l=e.side%2,n=De[e.scale];null==n&&(e.scale=l?_e[1].scale:Ae,n=De[e.scale]);let i=n.time;e.size=P(e.size),e.space=P(e.space),e.rotate=P(e.rotate),e.incrs=P(e.incrs||(2==n.distr?Ke:i?1==he?ut:ft:Ze)),e.splits=P(e.splits||(i&&1==n.distr?Ve:3==n.distr?Wt:4==n.distr?Yt:At)),e.stroke=P(e.stroke),e.grid.stroke=P(e.grid.stroke),e.ticks.stroke=P(e.ticks.stroke);let o=e.values;e.values=V(o)&&!V(o[0])?P(o):i?V(o)?mt(Ne,pt(o,Be)):U(o)?function(e,t){let l=je(t);return(t,n)=>n.map((t=>l(e(t))))}(Ne,o):o||Ue:o||Pt,e.filter=P(e.filter||(3>n.distr?W:Lt)),e.font=Yl(e.font),e.labelFont=Yl(e.labelFont),e._size=e.size(r,null,t,0),e._space=e._rotate=e._incrs=e._found=e._splits=e._values=null,e._size>0&&(cl[t]=!0),e._el=Me("u-axis",I)}})),n?n instanceof HTMLElement?(n.appendChild(D),si()):n(r,si):si(),r}Fl.assign=Z,Fl.fmtNum=h,Fl.rangeNum=a,Fl.rangeLog=i,Fl.rangeAsinh=o,Fl.orient=$t,Fl.join=function(e,t){let l=new Set;for(let t=0;e.length>t;t++){let n=e[t][0],i=n.length;for(let e=0;i>e;e++)l.add(n[e])}let n=[Array.from(l).sort(((e,t)=>e-t))],i=n[0].length,o=new Map;for(let e=0;i>e;e++)o.set(n[0][e],e);for(let l=0;e.length>l;l++){let s=e[l],r=s[0];for(let e=1;s.length>e;e++){let u=s[e],a=Array(i).fill(void 0),c=t?t[l][e]:1,f=[];for(let e=0;u.length>e;e++){let t=u[e],l=o.get(r[e]);null===t?0!=c&&(a[l]=t,2==c&&f.push(l)):a[l]=t}$(a,f,i),n.push(a)}}return n},Fl.fmtDate=je,Fl.tzDate=function(e,t){let l;return"UTC"==t||"Etc/UTC"==t?l=new Date(+e+6e4*e.getTimezoneOffset()):t==Be?l=e:(l=new Date(e.toLocaleString("en-US",{timeZone:t})),l.setMilliseconds(e.getMilliseconds())),l},Fl.sync=Zt;{Fl.addGap=el,Fl.clipGaps=Qt;let e=Fl.paths={points:dl};e.linear=xl,e.stepped=function(e){const l=c(e.align,1),n=c(e.ascDesc,!1);return(e,i,o,s)=>$t(e,i,((r,u,a,c,f,h,d,p,m,g,x)=>{let w=r.pxRound,_=0==c.ori?ol:sl;const b={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},k=b.stroke,v=1*c.dir*(0==c.ori?1:-1);o=t(a,o,s,1),s=t(a,o,s,-1);let y=[],M=!1,S=w(d(a[1==v?o:s],f,x,m)),z=w(h(u[1==v?o:s],c,g,p)),D=z;_(k,z,S);for(let e=1==v?o:s;e>=o&&s>=e;e+=v){let t=a[e],i=w(h(u[e],c,g,p));if(null==t){null===t&&(el(y,D,i),M=!0);continue}let o=w(d(t,f,x,m));if(M){if(el(y,D,i),S!=o){let e=r.width*we/2,t=y[y.length-1];t[0]+=n||1==l?e:-e,t[1]-=n||-1==l?e:-e}M=!1}1==l?_(k,i,S):_(k,D,o),_(k,i,o),S=o,D=i}if(null!=r.fill){let t=b.fill=new Path2D(k),l=w(d(r.fillTo(e,i,r.min,r.max),f,x,m));_(t,D,l),_(t,z,l)}return b.gaps=y=r.gaps(e,i,o,s,y),r.spanGaps||(b.clip=Qt(y,c.ori,p,m,g,x)),e.bands.length>0&&(b.band=Xt(e,i,o,s,k)),b}))},e.bars=function(e){const t=c((e=e||N).size,[.6,z,1]),l=e.align||0,n=(e.gap||0)*we,i=c(e.radius,0)*we,o=1-t[0],s=c(t[1],z)*we,r=c(t[2],1)*we,u=e.disp,a=c(e.each,(()=>{}));return(e,t,c,f)=>$t(e,t,((h,d,p,g,x,w,k,v,y,M,S)=>{let z=h.pxRound;const D=g.dir*(0==g.ori?1:-1),E=x.dir*(1==x.ori?1:-1);let T,P,A=0==g.ori?rl:ul,W=0==g.ori?a:(e,t,l,n,i,o,s)=>{a(e,t,l,i,n,s,o)},Y=k(h.fillTo(e,t,h.min,h.max),x,S,y),C=z(h.width*we),F=!1,H=null,R=null,L=null,I=null;if(null!=u){null!=u.fill&&null!=u.stroke&&(F=!0,H=u.fill.values(e,t,c,f),R=new Map,new Set(H).forEach((e=>{null!=e&&R.set(e,new Path2D)})),L=u.stroke.values(e,t,c,f),I=new Map,new Set(L).forEach((e=>{null!=e&&I.set(e,new Path2D)}))),d=u.x0.values(e,t,c,f),2==u.x0.unit&&(d=d.map((t=>e.posToVal(v+t*M,g.key,!0))));let l=u.size.values(e,t,c,f);P=2==u.size.unit?l[0]*M:w(l[0],g,M,v)-w(0,g,M,v),P=z(P-C),T=1==D?-C/2:P+C/2}else{let e=M;if(d.length>1){let t=null;for(let l=0,n=1/0;d.length>l;l++)if(void 0!==p[l]){if(null!=t){let i=m(d[l]-d[t]);n>i&&(n=i,e=m(w(d[l],g,M,v)-w(d[t],g,M,v)))}t=l}}P=z(_(s,b(r,e-e*o))-C-n),T=(0==l?P/2:l==D?0:P)-l*D*n/2}const G={stroke:null,fill:null,clip:null,band:null,gaps:null,flags:3},O=e.bands.length>0;let N;O&&(G.band=new Path2D,N=z(k(x.max,x,S,y)));const j=F?null:new Path2D,B=G.band;for(let l=1==D?c:f;l>=c&&f>=l;l+=D){let n=p[l],o=w(2!=g.distr||null!=u?d[l]:l,g,M,v),s=k(n,x,S,y),r=z(o-T),a=z(b(s,Y)),c=z(_(s,Y)),f=a-c;null!=p[l]&&(F?(C>0&&null!=L[l]&&A(I.get(L[l]),r,c,P,f,i*P),null!=H[l]&&A(R.get(H[l]),r,c,P,f,i*P)):A(j,r,c,P,f,i*P),W(e,t,l,r-C/2,c-C/2,P+C,f+C)),O&&(1==E?(a=c,c=N):(c=a,a=N),f=a-c,A(B,r-C/2,c+C/2,P+C,f-C,0))}return C>0&&(G.stroke=F?I:j),G.fill=F?R:j,G}))},e.spline=function(){return function(e){return(l,n,i,o)=>$t(l,n,((s,r,u,a,c,f,h,d,p,m,g)=>{let x,w,_,b=s.pxRound;0==a.ori?(x=nl,_=ol,w=fl):(x=il,_=sl,w=hl);const k=1*a.dir*(0==a.ori?1:-1);i=t(u,i,o,1),o=t(u,i,o,-1);let v=[],y=!1,M=b(f(r[1==k?i:o],a,m,d)),S=M,z=[],D=[];for(let e=1==k?i:o;e>=i&&o>=e;e+=k){let t=u[e],l=f(r[e],a,m,d);null!=t?(y&&(el(v,S,l),y=!1),z.push(S=l),D.push(h(u[e],c,g,p))):null===t&&(el(v,S,l),y=!0)}const E={stroke:e(z,D,x,_,w,b),fill:null,clip:null,band:null,gaps:null,flags:1},T=E.stroke;if(null!=s.fill&&null!=T){let e=E.fill=new Path2D(T),t=b(h(s.fillTo(l,n,s.min,s.max),c,g,p));_(e,S,t),_(e,M,t)}return E.gaps=v=s.gaps(l,n,i,o,v),s.spanGaps||(E.clip=Qt(v,a.ori,d,p,m,g)),l.bands.length>0&&(E.band=Xt(l,n,i,o,T)),E}))}(wl)}}return Fl}(); 3 | -------------------------------------------------------------------------------- /media/lib/uPlot.min.css: -------------------------------------------------------------------------------- 1 | .uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;} -------------------------------------------------------------------------------- /media/lib/uplot-component.js: -------------------------------------------------------------------------------- 1 | Vue.component('uplot-vue', { 2 | name: 'uplot-vue', 3 | props: { 4 | options: {type: Object, required: true}, 5 | data: {type: Array, required: true}, 6 | target: { 7 | validator(target) { 8 | return target == null || target instanceof HTMLElement || typeof target === 'function'; 9 | }, 10 | default: undefined, 11 | required: false 12 | } 13 | }, 14 | data() { 15 | // eslint-disable-next-line 16 | return {_chart: null, div_: null, width_:0, height_:0}; 17 | }, 18 | watch: { 19 | options(options, prevOptions) { 20 | if (!this._chart) { 21 | this._destroy(); 22 | this._create(); 23 | } else if (this.width_ != options.width || this.height_ != options.height) { 24 | this._chart.setSize({width: options.width, height: options.height}); 25 | } 26 | this.width_ = options.width; 27 | this.height_ = options.height; 28 | }, 29 | target() { 30 | this._destroy(); 31 | this._create(); 32 | }, 33 | data(data, prevData) { 34 | if (!this._chart) { 35 | this._create(); 36 | } else if ((0 in data)) { 37 | this._chart.setData(data); 38 | } 39 | this._resize(); 40 | } 41 | }, 42 | mounted() { 43 | this._create(); 44 | }, 45 | beforeUnmount() { 46 | this._destroy(); 47 | }, 48 | beforeDestroy() { 49 | this._destroy(); 50 | }, 51 | methods: { 52 | _destroy() { 53 | if (this._chart) { 54 | this.$emit('delete', this._chart); 55 | this._chart.destroy(); 56 | this._chart = null; 57 | } 58 | }, 59 | _create() { 60 | this.div_ = this.$props.target || this.$refs.targetRef; 61 | this._chart = new uPlot(this.$props.options, this.$props.data, this.div_); 62 | if(this.$props.options.cursor && "sync" in this.$props.options.cursor) window.cursorSync.sub(this._chart); 63 | this.width_ = this.$props.options.width; 64 | this.height_ = this.$props.options.height; 65 | this.$emit('create', this._chart); 66 | window.addEventListener("resize", e => { this._resize(); }); 67 | }, 68 | _resize() { 69 | if(!this._chart) return; 70 | let parentWidth = this.div_.offsetWidth; 71 | if(parentWidth != this.width_){ 72 | this.width_ = parentWidth; 73 | this._chart.setSize({width: this.width_, height: this.height_}); 74 | } 75 | } 76 | }, 77 | render(h) { 78 | return this.$props.target ? null : (Vue.createVNode ? Vue.createVNode : h)('div', { 79 | ref: 'targetRef' 80 | }); 81 | } 82 | }); -------------------------------------------------------------------------------- /media/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void setup() { 4 | // put your setup code here, to run once: 5 | pinMode(LED_BUILTIN, OUTPUT); 6 | Serial.begin(115200); 7 | Serial.println("setup"); 8 | Serial.println("test"); 9 | } 10 | 11 | float i=0; 12 | void loop() { 13 | Serial.println("loop"); 14 | // put your main code here, to run repeatedly: 15 | digitalWrite(LED_BUILTIN, HIGH); 16 | Serial.println(">led:1"); 17 | delay(50); 18 | digitalWrite(LED_BUILTIN, LOW); 19 | Serial.println(">led:0"); 20 | delay(50); 21 | i+=0.1; 22 | Serial.print(">sin:"); 23 | Serial.println(sin(i)); 24 | } -------------------------------------------------------------------------------- /media/main.js: -------------------------------------------------------------------------------- 1 | // Init Vue 2 | 3 | var vscode = null; 4 | if("acquireVsCodeApi" in window) vscode = acquireVsCodeApi(); 5 | 6 | var connections = []; 7 | var widgets = []; 8 | var telemetries = {}; 9 | var commands = {}; 10 | var logs = []; 11 | var telemBuffer = {}; 12 | var logBuffer = []; 13 | var app = new Vue({ 14 | el: '#app', 15 | data: { 16 | connections: connections, 17 | widgets: widgets, 18 | telemetries: telemetries, 19 | commands: commands, 20 | logs: logs, 21 | dataAvailable: false, 22 | cmdAvailable: false, 23 | logAvailable: false, 24 | telemRate: 0, 25 | logRate: 0, 26 | viewDuration: 15, 27 | leftPanelVisible: true, 28 | rightPanelVisible: false, 29 | textToSend: "", 30 | sendTextLineEnding: "\\r\\n", 31 | newChartDropZoneOver: false, 32 | newConnectionAddress: "", 33 | creatingConnection: false, 34 | telemetryFilterString: "", 35 | isViewPaused: false 36 | }, 37 | methods: { 38 | updateStats: function(widget){ 39 | widget.updateStats(); 40 | //Vue.set(telem, "stats", computeStats(telem.data)) 41 | }, 42 | sendCmd: function(cmd) { 43 | let command = `|${cmd.name}|`; 44 | if("params" in cmd) command += `${cmd.params}|`; 45 | for(let conn of app.connections){ 46 | conn.sendServerCommand({ id: this.id, cmd: command}); 47 | } 48 | }, 49 | onLogClick: function(log, index) { 50 | for(l of app.logs) l.selected = log.timestamp > 0 && l.timestamp == log.timestamp; 51 | logCursor.pub(log); 52 | }, 53 | showLeftPanel: function(show) { 54 | app.leftPanelVisible=show; 55 | triggerChartResize(); 56 | }, 57 | showRightPanel: function(show) { 58 | app.rightPanelVisible=show; 59 | triggerChartResize(); 60 | }, 61 | clearAll: function() { 62 | for(let w of widgets) w.destroy(); 63 | widgets.length = 0; 64 | Vue.set(app, 'widgets', widgets); 65 | logs.length = 0; 66 | Vue.set(app, 'logs', logs); 67 | logBuffer.length = 0; 68 | telemetries = {}; 69 | Vue.set(app, 'telemetries', telemetries); 70 | commands = {}; 71 | Vue.set(app, 'commands', commands); 72 | telemBuffer = {}; 73 | app.dataAvailable = false; 74 | app.cmdAvailable = false; 75 | app.logAvailable = false; 76 | app.isViewPaused = false; 77 | app.telemetryFilterString = ""; 78 | }, 79 | sendText: function(text) { 80 | let escape = app.sendTextLineEnding.replace("\\n","\n"); 81 | escape = escape.replace("\\r","\r"); 82 | vscode.postMessage({ cmd: "sendToSerial", text: text+escape}); 83 | }, 84 | onDragTelemetry: function(e, telemetryName){ 85 | e.dataTransfer.dropEffect = 'copy' 86 | e.dataTransfer.effectAllowed = 'copy' 87 | e.dataTransfer.setData("telemetryName", telemetryName); 88 | }, 89 | onDropInWidget: function(e, widget){ 90 | e.preventDefault(); 91 | e.stopPropagation(); 92 | widget.draggedOver = false; 93 | let telemetryName = e.dataTransfer.getData("telemetryName"); 94 | let newIsXY = app.telemetries[telemetryName].xy; 95 | let chartIsXY = (widget.series.length 96 | && widget.series[0].sourceNames.length 97 | && app.telemetries[widget.series[0].sourceNames[0]].xy 98 | ); 99 | if(newIsXY != chartIsXY) return; 100 | let serie = new DataSerie(telemetryName); 101 | serie.addSource(telemetryName); 102 | widget.addSerie(serie); 103 | }, 104 | onWidgetDragOver: function(e, widget){ 105 | e.preventDefault(); 106 | widget.draggedOver = true; 107 | }, 108 | onWidgetDragLeave: function(e, widget){ 109 | e.preventDefault(); 110 | widget.draggedOver = false; 111 | }, 112 | showWidget: function(widget, show){ 113 | widget.hidden = show; 114 | console.log(widget, show) 115 | triggerChartResize(); 116 | }, 117 | removeWidget: function(widget){ 118 | widget.destroy(); 119 | let idx = widgets.findIndex((w)=>w.id==widget.id); 120 | if(idx>=0) app.widgets.splice(idx, 1); 121 | triggerChartResize(); 122 | }, 123 | onDropInNewChart: function(e, prepend=true){ 124 | e.preventDefault(); 125 | e.stopPropagation(); 126 | app.newChartDropZoneOver = false; 127 | let telemetryName = e.dataTransfer.getData("telemetryName"); 128 | let chart = new ChartWidget(!!app.telemetries[telemetryName].xy); 129 | let serie = new DataSerie(telemetryName); 130 | serie.addSource(telemetryName); 131 | chart.addSerie(serie); 132 | if(prepend) widgets.unshift(chart); 133 | else widgets.push(chart); 134 | }, 135 | onNewChartDragOver: function(e){ 136 | e.preventDefault(); 137 | app.newChartDropZoneOver = true; 138 | }, 139 | onNewChartDragLeave: function(e){ 140 | e.preventDefault(); 141 | app.newChartDropZoneOver = false; 142 | }, 143 | createConnection: function(address_=undefined, port_=undefined){ 144 | let conn = new ConnectionTeleplotWebsocket(); 145 | let addr = address_ || app.newConnectionAddress; 146 | let port = port_ || 8080; 147 | if(addr.includes(":")) { 148 | port = parseInt(addr.split(":")[1]); 149 | addr = addr.split(":")[0]; 150 | } 151 | conn.connect(addr, port); 152 | app.connections.push(conn); 153 | app.creatingConnection = false; 154 | app.newConnectionAddress = ""; 155 | }, 156 | removeConnection: function(conn){ 157 | for(let i=0;i str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 168 | let rule = app.telemetryFilterString.split("*").map(escapeRegex).join(".*"); 169 | rule = "^" + rule + "$"; 170 | let regex = new RegExp(rule); 171 | return regex.test(name); 172 | }, 173 | updateWidgetSize: function(widget){ 174 | updateWidgetSize_(widget); 175 | }, 176 | isWidgetSmallOnGrid: function(widget){ 177 | if(widget.gridPos.w < 3) return true; 178 | if(widget.gridPos.w < 5 && widget.series.length > 1) return true; 179 | return false; 180 | }, 181 | shouldShowRightPanelButton: function(){ 182 | if(app.rightPanelVisible) return false; 183 | if(app.cmdAvailable || app.logAvailable) return true; 184 | // Show with connected serial inputs 185 | for(let conn of app.connections){ 186 | if(conn.connected){ 187 | for(let input of conn.inputs){ 188 | if(input.type == "serial" && input.connected){ 189 | return true; 190 | } 191 | } 192 | } 193 | } 194 | return false; 195 | } 196 | } 197 | }) 198 | 199 | function updateWidgetSize_(widget){ 200 | const clamp = (num, min, max) => Math.min(Math.max(num, min), max); 201 | widget.gridPos.w = clamp(widget.gridPos.w, 1, 12); 202 | widget.gridPos.h = clamp(widget.gridPos.h, 1, 20); 203 | widget.options.height = (widget.gridPos.h-1)*50; 204 | widget.forceUpdate = true; 205 | triggerChartResize(); 206 | } 207 | 208 | function sendCommand(cmd) { 209 | let command = `|${cmd.name}|`; 210 | if("params" in cmd) command += `${cmd.params}|`; 211 | for(let conn of app.connections){ 212 | conn.sendServerCommand({ id: this.id, cmd: command}); 213 | } 214 | } 215 | 216 | function rgba(r,g,b,a){ 217 | return {r,g,b,a, toString: function(){ return `rgba(${this.r},${this.g},${this.b},${this.a})`}}; 218 | } 219 | var ColorPalette = { 220 | colors: [ 221 | rgba(231, 76, 60,1.0), //red 222 | rgba(52, 152, 219,1.0), //blue 223 | rgba(46, 204, 113,1.0), //green 224 | rgba(155, 89, 182,1.0), //violet 225 | rgba(241, 196, 15,1.0), //yellow 226 | rgba(26, 188, 156,1.0), //turquoise 227 | rgba(230, 126, 34,1.0), //orange 228 | rgba(52, 73, 94,1.0), //blueish grey 229 | rgba(127, 140, 141,1.0), //gray 230 | rgba(192, 57, 43,1.0), //dark red 231 | rgba(41, 128, 185,1.0), //darkblue 232 | rgba(39, 174, 96,1.0), //darkgreen 233 | rgba(142, 68, 173,1.0), // darkviolet 234 | rgba(211, 84, 0,1.0), //darkorange 235 | rgba(44, 62, 80,1.0), //blueish darkgrey 236 | rgba(0, 0, 0,1.0), //black 237 | ], 238 | getColor: function(index, alpha=1.0){ 239 | let color = Object.assign({}, this.colors[index % this.colors.length]); 240 | color.a = alpha; 241 | return color; 242 | } 243 | } 244 | 245 | //Init refresh rate 246 | setInterval(updateView, 60); // 15fps 247 | 248 | logCursor = { 249 | cursor:{ 250 | show: true, 251 | sync:{ 252 | values:[0,0], 253 | scales:["x"], 254 | key: "cursorSync", 255 | filters: {pub: function(...e){return true}, sub: function(...e){return true}}, 256 | match: [function(a,b){return a==b}], 257 | setSeries: true, 258 | }, 259 | left: 10, 260 | top: 10, 261 | x: true, 262 | y: false 263 | }, 264 | scales: { 265 | x:{ori:0, _max: 1, _min: 1, key:"x", time:true}, 266 | }, 267 | clientX: -10, 268 | clientY: -10, 269 | pub: function(log) { 270 | logCursor.cursor.sync.values[0] = log.timestamp/1000; 271 | logCursor.cursor.sync.values[1] = 0; 272 | window.cursorSync.pub("mousemove", logCursor, 0, 0, 0, 0, -42); 273 | } 274 | }; 275 | 276 | // Init cursor sync 277 | var timestampWindow = {min:0, max:0}; 278 | window.cursorSync = uPlot.sync("cursorSync"); 279 | window.cursorSync.sub({ pub:function(type, self, x, y, w, h, i){ 280 | if(type=="mouseup"){ 281 | app.isViewPaused = true; 282 | } 283 | if(type=="mousemove"){ 284 | if(i != -42){ 285 | let timestamp = self.cursor.sync.values[0]; 286 | for(l of app.logs) l.selected = Math.abs(l.timestamp/1000 - timestamp) < 0.1; // within 10ms difference (20ms window) 287 | } 288 | if(i != null) updateDisplayedVarValues(self.cursor.sync.values[0], self.cursor.sync.values[1]); 289 | else resetDisplayedVarValues(); 290 | } 291 | // let some time to update the axes min/max 292 | setTimeout(()=>{ 293 | timestampWindow.min = self.scales.x._min; 294 | timestampWindow.max = self.scales.x._max; 295 | }, 10); 296 | return true; 297 | }}); 298 | 299 | var defaultPlotOpts = { 300 | title: "", 301 | width: 400, 302 | height: 250, 303 | //hooks: {setCursor: [function(e){console.log(e);}]}, 304 | scales: { 305 | x: { 306 | time: true 307 | }, 308 | y:{} 309 | }, 310 | series: [ 311 | {}, 312 | { 313 | stroke: "red", 314 | fill: "rgba(255,0,0,0.1)" 315 | } 316 | ], 317 | cursor: { 318 | lock: false, 319 | focus: { prox: 16, }, 320 | sync: { 321 | key: window.cursorSync.key, 322 | setSeries: true 323 | } 324 | }, 325 | legend: { 326 | show: false 327 | } 328 | }; 329 | 330 | const drawXYPoints = (u, seriesIdx, idx0, idx1) => { 331 | const size = 5 * devicePixelRatio; 332 | uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => { 333 | let d = u.data[seriesIdx]; 334 | u.ctx.fillStyle = series.stroke(); 335 | let deg360 = 2 * Math.PI; 336 | let strokeStylebackup = u.ctx.strokeStyle; 337 | let lineWidthbackup = u.ctx.lineWidth; 338 | //let p = new Path2D(); 339 | let lastX=0, lastY=0; 340 | for (let i = 0; i < d[0].length; i++) { 341 | let xVal = d[0][i]; 342 | let yVal = d[1][i]; 343 | if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) { 344 | let cx = valToPosX(xVal, scaleX, xDim, xOff); 345 | let cy = valToPosY(yVal, scaleY, yDim, yOff); 346 | /*p.moveTo(cx + size/2, cy); 347 | arc(p, cx, cy, size/2, 0, deg360);*/ 348 | u.ctx.beginPath(); 349 | u.ctx.globalAlpha = (1.0/d[0].length)*i; 350 | u.ctx.fillStyle = series.stroke(); 351 | u.ctx.strokeStyle = series.stroke(); 352 | u.ctx.lineWidth = 1*devicePixelRatio; 353 | if(i>0){ 354 | u.ctx.moveTo(lastX, lastY); 355 | u.ctx.lineTo(cx, cy); 356 | u.ctx.stroke(); 357 | } 358 | u.ctx.beginPath(); 359 | u.ctx.ellipse(cx, cy, size/2, size/2, 0, 0, deg360); 360 | u.ctx.fill(); 361 | 362 | lastX = cx; 363 | lastY = cy; 364 | } 365 | } 366 | u.ctx.strokeStyle = strokeStylebackup; 367 | u.ctx.lineWidth = lineWidthbackup; 368 | }); 369 | return null; 370 | }; 371 | 372 | var ConnectionCount = 0; 373 | class Connection{ 374 | constructor(){ 375 | this.name = ""; 376 | this.id = "connection-"+ConnectionCount++; 377 | this.type = ""; 378 | this.connected = false; 379 | this.inputs = []; 380 | } 381 | 382 | connect(){ 383 | 384 | } 385 | 386 | removeInput(input){ 387 | for(let i=0;i { 416 | let msg = message.data; 417 | if("id" in msg){ 418 | for(let input of this.inputs){ 419 | if(input.id == msg.id){ 420 | input.onMessage(msg); 421 | break; 422 | } 423 | } 424 | } 425 | else{ 426 | if("data" in msg) { 427 | parseData(msg); //update server so it keeps track of connection IDs when forwarding data 428 | } 429 | else if("cmd" in msg) { 430 | //nope 431 | } 432 | } 433 | }); 434 | this.vscode.postMessage({ cmd: "listSerialPorts"}); 435 | //Report UDP input as connected 436 | this.udp.connected = true; 437 | this.connected = true; 438 | return true; 439 | } 440 | 441 | disconnect() { 442 | for(let input of this.inputs){ 443 | input.disconnect(); 444 | } 445 | this.connected = false; 446 | } 447 | 448 | sendServerCommand(command) { 449 | this.vscode.postMessage(command); 450 | } 451 | 452 | sendCommand(command) { 453 | for(let input of this.inputs){ 454 | input.sendCommand(command); 455 | } 456 | } 457 | 458 | updateCMDList() { 459 | for(let input of this.inputs){ 460 | input.updateCMDList(); 461 | } 462 | } 463 | 464 | createInput(type) { 465 | if(type=="serial") { 466 | let serialIn = new DataInputSerial(this, "Serial"); 467 | this.inputs.push(serialIn); 468 | } 469 | } 470 | } 471 | 472 | class ConnectionTeleplotWebsocket extends Connection{ 473 | constructor(){ 474 | super(); 475 | this.name="" 476 | this.type = "teleplot-websocket"; 477 | this.inputs = []; 478 | this.socket = null; 479 | this.address = ""; 480 | this.port = ""; 481 | this.udp = new DataInputUDP(this, "UDP"); 482 | this.udp.address = ""; 483 | this.udp.port = 47269; 484 | this.inputs.push(this.udp); 485 | } 486 | 487 | connect(_address, _port){ 488 | this.name = _address+":"+_port; 489 | this.address = _address; 490 | this.port = _port; 491 | this.udp.address = this.address; 492 | this.socket = new WebSocket("ws://"+this.address+":"+this.port); 493 | this.socket.onopen = (event) => { 494 | this.udp.connected = true; 495 | this.connected = true; 496 | this.sendServerCommand({ cmd: "listSerialPorts"}); 497 | }; 498 | this.socket.onclose = (event) => { 499 | this.udp.connected = false; 500 | this.connected = false; 501 | for(let input of this.inputs){ 502 | input.disconnect(); 503 | } 504 | setTimeout(()=>{ 505 | this.connect(this.address, this.port); 506 | }, 2000); 507 | }; 508 | this.socket.onmessage = (msgWS) => { 509 | let msg = JSON.parse(msgWS.data); 510 | if("id" in msg){ 511 | for(let input of this.inputs){ 512 | if(input.id == msg.id){ 513 | input.onMessage(msg); 514 | break; 515 | } 516 | } 517 | } 518 | else{ 519 | this.udp.onMessage(msg); 520 | } 521 | }; 522 | return true; 523 | } 524 | 525 | disconnect(){ 526 | if(this.socket){ 527 | this.socket.close(); 528 | this.socket = null; 529 | } 530 | } 531 | 532 | sendServerCommand(command){ 533 | if(this.socket) this.socket.send(JSON.stringify(command)); 534 | } 535 | 536 | updateCMDList(){ 537 | for(let input of this.inputs){ 538 | input.updateCMDList(); 539 | } 540 | } 541 | 542 | createInput(type) { 543 | if(type=="serial") { 544 | let serialIn = new DataInputSerial(this, "Serial"); 545 | this.inputs.push(serialIn); 546 | } 547 | } 548 | } 549 | 550 | var DataInputCount = 0; 551 | class DataInput{ 552 | constructor(_connection, _name){ 553 | this.connection = _connection; 554 | this.name = _name; 555 | this.id = "data-input-"+DataInputCount++; 556 | this.type = ""; 557 | this.connected = false; 558 | } 559 | } 560 | 561 | class DataInputUDP extends DataInput{ 562 | constructor(_connection, _name) { 563 | super(_connection, _name); 564 | this.type = "UDP"; 565 | this.address = ""; 566 | this.port = 47269; 567 | } 568 | 569 | connect(){} 570 | disconnect(){} 571 | 572 | onMessage(msg){ 573 | if("data" in msg) { 574 | msg.input = this; 575 | parseData(msg); 576 | } 577 | else if("cmd" in msg) { 578 | //nope 579 | } 580 | } 581 | 582 | sendCommand(command){ 583 | this.connection.sendServerCommand({ id: this.id, cmd: command}); 584 | } 585 | 586 | updateCMDList(){ 587 | this.sendCommand("|_telecmd_list_cmd|"); 588 | } 589 | } 590 | 591 | class DataInputSerial extends DataInput{ 592 | constructor(_connection, _name) { 593 | super(_connection, _name); 594 | this.port = null; 595 | this.baudrate = 115200; 596 | this.type = "serial"; 597 | this.portList = []; 598 | this.listPorts(); 599 | this.textToSend = ""; 600 | this.endlineToSend = ""; 601 | } 602 | 603 | connect(){ 604 | let baud = parseInt(this.baudrate); 605 | this.connection.sendServerCommand({ id: this.id, cmd: "connectSerialPort", port: this.port, baud: baud}) 606 | } 607 | 608 | disconnect(){ 609 | this.connection.sendServerCommand({ id: this.id, cmd: "disconnectSerialPort"}) 610 | } 611 | 612 | onMessage(msg){ 613 | if("data" in msg) { 614 | msg.input = this; 615 | parseData(msg); 616 | } 617 | else if("cmd" in msg) { 618 | if(msg.cmd == "serialPortList"){ 619 | this.portList.length = 0; 620 | for(let serial of msg.list){ 621 | if( serial.locationId 622 | || serial.serialNumber 623 | || serial.pnpId 624 | || serial.vendorId 625 | || serial.productId ){ 626 | this.portList.push(serial); 627 | } 628 | } 629 | } 630 | else if(msg.cmd == "serialPortConnect"){ 631 | this.connected = true; 632 | app.showRightPanel(true); 633 | } 634 | else if(msg.cmd == "serialPortDisconnect"){ 635 | this.connected = false; 636 | } 637 | } 638 | } 639 | 640 | listPorts(){ 641 | this.connection.sendServerCommand({ id: this.id, cmd: "listSerialPorts"}); 642 | } 643 | 644 | sendCommand(){ 645 | //nope 646 | } 647 | 648 | updateCMDList(){ 649 | //nope 650 | } 651 | 652 | sendText(text, lineEndings) { 653 | let escape = lineEndings.replace("\\n","\n"); 654 | escape = escape.replace("\\r","\r"); 655 | this.connection.sendServerCommand({ id: this.id, cmd: "sendToSerial", text: text+escape}); 656 | } 657 | } 658 | 659 | var DataSerieIdCount = 0; 660 | class DataSerie{ 661 | constructor(_name){ 662 | this.name = _name; 663 | this.sourceNames = []; 664 | this.formula = ""; 665 | this.initialized = false; 666 | this.data = [[],[]]; 667 | this.pendingData = [[],[]]; 668 | this.options = {}; 669 | this.value = null; 670 | this.id = "data-serie-" + DataSerieIdCount++; 671 | this.stats = null; 672 | } 673 | 674 | destroy(){ 675 | for(let name of this.sourceNames){ 676 | onTelemetryUnused(name); 677 | } 678 | this.sourceNames.length = 0; 679 | } 680 | 681 | addSource(name){ 682 | this.sourceNames.push(name); 683 | onTelemetryUsed(name); 684 | } 685 | 686 | update(){ 687 | this.applyTimeWindow(); 688 | // no formula, simple data reference 689 | if(this.formula=="" && this.sourceNames.length==1 && app.telemetries[this.sourceNames[0]]){ 690 | let isXY = app.telemetries[this.sourceNames[0]].xy; 691 | this.data[0] = app.telemetries[this.sourceNames[0]].data[0]; 692 | this.data[1] = app.telemetries[this.sourceNames[0]].data[1]; 693 | if(isXY) this.data[2] = app.telemetries[this.sourceNames[0]].data[2]; 694 | this.pendingData[0] = app.telemetries[this.sourceNames[0]].pendingData[0]; 695 | this.pendingData[1] = app.telemetries[this.sourceNames[0]].pendingData[1]; 696 | if(isXY) this.pendingData[2] = app.telemetries[this.sourceNames[0]].pendingData[2]; 697 | this.value = app.telemetries[this.sourceNames[0]].value; 698 | } 699 | } 700 | 701 | updateStats(){ 702 | this.stats = computeStats(this.data); 703 | } 704 | 705 | applyTimeWindow(){ 706 | if(parseFloat(app.viewDuration)<=0) return; 707 | for(let key of this.sourceNames) { 708 | if(app.telemetries[key] == undefined) continue; 709 | let d = app.telemetries[key].data; 710 | let timeIdx = 0; 711 | if(app.telemetries[key].xy) timeIdx = 2; 712 | let latestTimestamp = d[timeIdx][d[timeIdx].length-1]; 713 | let minTimestamp = latestTimestamp - parseFloat(app.viewDuration); 714 | let minIdx = findClosestLowerByIdx(d[timeIdx], minTimestamp); 715 | if(d[timeIdx][minIdx]0) this.updatedForZeroLength = false; 840 | } 841 | } 842 | } 843 | 844 | function parseData(msgIn){ 845 | if(app.isViewPaused) return; // Do not buffer incomming data while paused 846 | let now = new Date().getTime(); 847 | let fromSerial = msgIn.fromSerial || (msgIn.input && msgIn.input.type=="serial"); 848 | if(fromSerial) now = msgIn.timestamp; 849 | //parse msg 850 | let msgList = (""+msgIn.data).split("\n"); 851 | for(let msg of msgList){ 852 | try{ 853 | // Inverted logic on serial port for usability 854 | if(fromSerial && msg.startsWith(">")) msg = msg.substring(1);// remove '>' to consider as variable 855 | else if(fromSerial && !msg.startsWith(">")) msg = ">:"+msg;// add '>' to consider as log 856 | 857 | // Command 858 | if(msg.startsWith("|")){ 859 | // Parse command list 860 | let cmdList = msg.split("|"); 861 | for(let cmd of cmdList){ 862 | if(cmd.length==0) continue; 863 | if(cmd.startsWith("_")) continue; 864 | if(app.commands[cmd] == undefined){ 865 | let newCmd = { 866 | name: cmd 867 | }; 868 | Vue.set(app.commands, cmd, newCmd); 869 | } 870 | } 871 | if(!app.cmdAvailable && Object.entries(app.commands).length>0) app.cmdAvailable = true; 872 | } 873 | // Log 874 | else if(msg.startsWith(">")){ 875 | let currLog = { 876 | timestamp: now, 877 | text: "" 878 | } 879 | 880 | let logStart = msg.indexOf(":")+1; 881 | currLog.text = msg.substr(logStart); 882 | currLog.timestamp = parseFloat(msg.substr(1, logStart-2)); 883 | if(isNaN(currLog.timestamp) || !isFinite(currLog.timestamp)) currLog.timestamp = now; 884 | logBuffer.unshift(currLog);//prepend log to buffer 885 | } 886 | // Data 887 | else { 888 | // Extract series 889 | if(!msg.includes(':')) return; 890 | let startIdx = msg.indexOf(':'); 891 | let name = msg.substr(0,msg.indexOf(':')); 892 | let endIdx = msg.indexOf('|'); 893 | let flags = msg.substr(endIdx+1); 894 | if(endIdx == -1){ 895 | flags = "g"; 896 | endIdx = msg.length; 897 | } 898 | // Extract values array 899 | let values = msg.substr(startIdx+1, endIdx-startIdx-1).split(';') 900 | let xArray = []; 901 | let yArray = []; 902 | let zArray = []; 903 | for(let value of values){ 904 | if(value.length==0) continue; 905 | let dims = value.split(":"); 906 | if(dims.length == 1){ 907 | xArray.push(now); 908 | yArray.push(parseFloat(dims[0])); 909 | } 910 | else if(dims.length == 2){ 911 | xArray.push(parseFloat(dims[0])); 912 | yArray.push(parseFloat(dims[1])); 913 | zArray.push(now); 914 | } 915 | else if(dims.length == 3){ 916 | xArray.push(parseFloat(dims[0])); 917 | yArray.push(parseFloat(dims[1])); 918 | zArray.push(parseFloat(dims[2])); 919 | } 920 | /*let sepIdx = value.indexOf(':'); 921 | if(sepIdx==-1){ 922 | xArray.push(now); 923 | yArray.push(parseFloat(value)); 924 | } 925 | else { 926 | xArray.push(parseFloat(value.substr(0, sepIdx))); 927 | yArray.push(parseFloat(value.substr(sepIdx+1))); 928 | }*/ 929 | } 930 | if(xArray.length>0){ 931 | appendData(name, xArray, yArray, zArray, flags) 932 | } 933 | } 934 | } 935 | catch(e){console.log(e)} 936 | } 937 | } 938 | 939 | function appendData(key, valuesX, valuesY, valuesZ, flags) { 940 | if(key.substring(0, 6) === "statsd") return; 941 | let isTimeBased = !flags.includes("xy"); 942 | let shouldPlot = !flags.includes("np"); 943 | if(app.telemetries[key] == undefined){ 944 | let config = Object.assign({}, defaultPlotOpts); 945 | config.name = key; 946 | config.scales.x.time = isTimeBased; 947 | if(!isTimeBased){ 948 | config.mode = 2; 949 | config.cursor.sync = undefined; 950 | config.series[1].paths = drawXYPoints; 951 | } 952 | var obj = { 953 | name: key, 954 | flags: flags, 955 | data: [[],[]], 956 | pendingData: [[],[]], 957 | value: 0, 958 | config: config, 959 | xy: !isTimeBased, 960 | usageCount: 0 961 | }; 962 | if(!isTimeBased){ 963 | obj.data.push([]); 964 | obj.pendingData.push([]); 965 | } 966 | Vue.set(app.telemetries, key, obj) 967 | // Create widget 968 | if(shouldPlot){ 969 | let chart = new ChartWidget(!isTimeBased); 970 | let serie = new DataSerie(key); 971 | serie.addSource(key); 972 | chart.addSerie(serie); 973 | widgets.push(chart); 974 | } 975 | } 976 | if(telemBuffer[key] == undefined){ 977 | telemBuffer[key] = {data:[[],[]], value:0}; 978 | if(!isTimeBased) telemBuffer[key].data.push([]); 979 | } 980 | 981 | // Convert timestamps to seconds 982 | if(isTimeBased) { valuesX.forEach((elem, idx, arr)=>arr[idx] = elem/1000); } 983 | else { valuesZ.forEach((elem, idx, arr)=>arr[idx] = elem/1000); } 984 | 985 | // Flush data into buffer (to be flushed by updateView) 986 | telemBuffer[key].data[0].push(...valuesX); 987 | telemBuffer[key].data[1].push(...valuesY); 988 | if(app.telemetries[key].xy) { 989 | telemBuffer[key].value = ""+valuesX[valuesX.length-1].toFixed(4)+" "+valuesY[valuesY.length-1].toFixed(4)+""; 990 | telemBuffer[key].data[2].push(...valuesZ); 991 | } 992 | else { 993 | telemBuffer[key].value = valuesY[valuesY.length-1]; 994 | } 995 | return; 996 | } 997 | 998 | var lastUpdateViewTimestamp = 0; 999 | function updateView() { 1000 | // Clear Telemetries pendingData 1001 | for(let key in app.telemetries) { 1002 | app.telemetries[key].pendingData[0].length = 0; 1003 | app.telemetries[key].pendingData[1].length = 0; 1004 | if(app.telemetries[key].xy) app.telemetries[key].pendingData[2].length = 0; 1005 | } 1006 | // Flush Telemetry buffer into app model 1007 | let dataSum = 0; 1008 | if(!app.isViewPaused){ 1009 | for(let key in telemBuffer) { 1010 | if(telemBuffer[key].data[0].length == 0) continue; // nothing to flush 1011 | dataSum += telemBuffer[key].data[0].length; 1012 | app.telemetries[key].data[0].push(...telemBuffer[key].data[0]); 1013 | app.telemetries[key].data[1].push(...telemBuffer[key].data[1]); 1014 | if(app.telemetries[key].xy) app.telemetries[key].data[2].push(...telemBuffer[key].data[2]); 1015 | app.telemetries[key].pendingData[0].push(...telemBuffer[key].data[0]); 1016 | app.telemetries[key].pendingData[1].push(...telemBuffer[key].data[1]); 1017 | if(app.telemetries[key].xy) app.telemetries[key].pendingData[2].push(...telemBuffer[key].data[2]); 1018 | telemBuffer[key].data[0].length = 0; 1019 | telemBuffer[key].data[1].length = 0; 1020 | if(app.telemetries[key].xy) telemBuffer[key].data[2].length = 0; 1021 | app.telemetries[key].value = telemBuffer[key].value 1022 | } 1023 | } 1024 | 1025 | //Clear older data from viewDuration 1026 | if(parseFloat(app.viewDuration)>0) 1027 | { 1028 | for(let key in app.telemetries) { 1029 | let data = app.telemetries[key].data; 1030 | let timeIdx = 0; 1031 | if(app.telemetries[key].xy) timeIdx = 2; 1032 | let latestTimestamp = data[timeIdx][data[timeIdx].length-1]; 1033 | let minTimestamp = latestTimestamp - parseFloat(app.viewDuration); 1034 | let minIdx = findClosestLowerByIdx(data[timeIdx], minTimestamp); 1035 | if(data[timeIdx][minIdx]0) app.dataAvailable = true; 1049 | 1050 | // Logs 1051 | var logSum = logBuffer.length; 1052 | if(!app.isViewPaused && logBuffer.length>0) { 1053 | app.logs.unshift(...logBuffer);//prepend log to list 1054 | logBuffer.length = 0; 1055 | } 1056 | if(!app.logAvailable && app.logs.length>0) app.logAvailable = true; 1057 | 1058 | // Stats 1059 | let now = new Date().getTime(); 1060 | if(lastUpdateViewTimestamp==0) lastUpdateViewTimestamp = now; 1061 | let diff = now - lastUpdateViewTimestamp 1062 | if(diff>0){ 1063 | app.telemRate = app.telemRate*0.8 + (1000/diff*dataSum)*0.2; 1064 | app.logRate = app.logRate *0.8 + (1000/diff*logSum)*0.2; 1065 | } 1066 | lastUpdateViewTimestamp = now; 1067 | } 1068 | 1069 | function exportSessionJSON() { 1070 | let content = JSON.stringify({ 1071 | telemetries: app.telemetries, 1072 | logs: app.logs, 1073 | dataAvailable: app.dataAvailable, 1074 | logAvailable: app.logAvailable 1075 | }); 1076 | let now = new Date(); 1077 | let filename = `teleplot_${now.getFullYear()}-${now.getMonth()}-${now.getDate()}_${now.getHours()}-${now.getMinutes()}.json`; 1078 | saveFile(content, filename); 1079 | } 1080 | 1081 | function exportSessionCSV() { 1082 | 1083 | let csv = "timestamp(ms),"; 1084 | let dataList = []; 1085 | for(let key in app.telemetries) { 1086 | csv += key+","; 1087 | dataList.push(app.telemetries[key].data); 1088 | } 1089 | csv += "\n"; 1090 | let joinedData = uPlot.join(dataList); 1091 | 1092 | for(let i=0;i{updateWidgetSize_(chart)}, 100); 1210 | widgets.push(chart); 1211 | } 1212 | } 1213 | app.leftPanelVisible = false; // hide telemetry list 1214 | } 1215 | catch(e) { 1216 | alert("Importation failed: "+e.toString()); 1217 | } 1218 | }; 1219 | reader.readAsText(file); 1220 | } 1221 | 1222 | var chartResizeTimeout = null; 1223 | function triggerChartResize(){ 1224 | if(chartResizeTimeout) clearTimeout(chartResizeTimeout); 1225 | chartResizeTimeout = setTimeout(()=>{ 1226 | window.dispatchEvent(new Event('resize')); 1227 | }, 100); 1228 | } 1229 | 1230 | function computeStats(data) { 1231 | let stats = { 1232 | min:0, 1233 | max:0, 1234 | sum:0, 1235 | mean:0, 1236 | med:0, 1237 | stdev:0, 1238 | }; 1239 | let values = data[1]; 1240 | //Find min/max indexes from timestampWindow 1241 | let minIdx = 0, maxIdx = data[1].length; 1242 | if(timestampWindow.min !=0 && timestampWindow.max != 0) 1243 | { 1244 | minIdx = findClosestLowerByIdx(data[0], timestampWindow.min) + 1; 1245 | maxIdx = findClosestLowerByIdx(data[0], timestampWindow.max); 1246 | if(maxIdx<=minIdx || maxIdx>=data[0].length) return stats; 1247 | values = data[1].slice(minIdx, maxIdx); 1248 | } 1249 | if(values.length==0) return stats; 1250 | // Sort 1251 | let arr = values.slice().sort(function(a, b){return a - b;}); 1252 | for(let i=0;i n); 1289 | if (isClosestLower || isLowerLast) { 1290 | return idx; 1291 | } 1292 | else { 1293 | if (arr[idx] > n) to = idx - 1; 1294 | else from = idx + 1; 1295 | } 1296 | } 1297 | return 0; 1298 | } 1299 | 1300 | function resetDisplayedVarValues(){ 1301 | //for each telem, set latest value 1302 | let telemList = Object.keys(app.telemetries); 1303 | for(let telemName of telemList) { 1304 | let telem = app.telemetries[telemName]; 1305 | if(telem.xy) continue; 1306 | let idx = telem.data[0].length-1; 1307 | if(0 <= idx && idx < telem.data[0].length) { 1308 | telem.value = telem.data[1][idx]; 1309 | } 1310 | } 1311 | } 1312 | function updateDisplayedVarValues(valueX, valueY){ 1313 | //for each telem, find closest value (before valueX and valueY) 1314 | let telemList = Object.keys(app.telemetries); 1315 | for(let telemName of telemList) { 1316 | let telem = app.telemetries[telemName]; 1317 | let timeIdx = 0; 1318 | if(telem.xy) { timeIdx = 2; } 1319 | let idx = findClosestLowerByIdx(telem.data[timeIdx], valueX); 1320 | if(idx >= telem.data[timeIdx].length) continue; 1321 | //Refine index, closer to timestamp 1322 | if(idx+1 < telem.data[timeIdx].length 1323 | && (valueX-telem.data[timeIdx][idx]) > (telem.data[timeIdx][idx+1]-valueX)){ 1324 | idx +=1; 1325 | } 1326 | if(idx < telem.data[timeIdx].length) { 1327 | if(telem.xy) { 1328 | app.telemetries[telemName].value = ""+telem.data[0][idx].toFixed(4)+" "+telem.data[1][idx].toFixed(4)+""; 1329 | } 1330 | else { 1331 | app.telemetries[telemName].value = telem.data[1][idx]; 1332 | } 1333 | } 1334 | } 1335 | } 1336 | 1337 | if(vscode){ 1338 | let conn = new ConnectionTeleplotVSCode(); 1339 | conn.connect(); 1340 | app.connections.push(conn); 1341 | } 1342 | else { 1343 | let conn = new ConnectionTeleplotWebsocket(); 1344 | let addr = window.location.hostname; 1345 | let port = window.location.port; 1346 | conn.connect(addr, port); 1347 | app.connections.push(conn); 1348 | } 1349 | 1350 | function onTelemetryUsed(name, force=false){ 1351 | let telem = app.telemetries[name]; 1352 | if(telem == undefined) return; 1353 | if(!force) telem.usageCount++; 1354 | } 1355 | 1356 | function onTelemetryUnused(name, force=false){ 1357 | let telem = app.telemetries[name]; 1358 | if(telem == undefined) return; 1359 | if(telem.usageCount>0)telem.usageCount--; 1360 | } 1361 | 1362 | setInterval(()=>{ 1363 | for(let conn of app.connections){ 1364 | conn.updateCMDList(); 1365 | } 1366 | }, 3000); 1367 | 1368 | -------------------------------------------------------------------------------- /media/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Verdana, Geneva, Tahoma, sans-serif; 3 | font-size: 12px; 4 | } 5 | 6 | button { 7 | background-color: #ecf0f1; 8 | border: 1px solid #bdc3c7; 9 | border-radius: 3px; 10 | color: #2c3e50; 11 | padding: 0.5em 0.5em; 12 | text-align: center; 13 | text-decoration: none; 14 | display: inline-block; 15 | cursor: pointer; 16 | font-size: 12px; 17 | } 18 | 19 | .top-banner { 20 | display: flex; 21 | flex-direction: row; 22 | flex-wrap: wrap; 23 | background-color: #2980b9; 24 | padding-top: 0px; 25 | padding-bottom: 0px; 26 | padding-left: 10px; 27 | padding-right: 10px; 28 | color: white; 29 | justify-content: space-between; 30 | align-items: center; 31 | min-height: 50px; 32 | } 33 | 34 | .top-banner-left { 35 | display: flex; 36 | flex-direction: row; 37 | align-items: center; 38 | height: 100%; 39 | justify-content: flex-start; 40 | width: 100%; 41 | flex: 1; 42 | } 43 | 44 | .top-banner-title { 45 | display: block; 46 | font-size: 30px; 47 | } 48 | 49 | #session-json-input { 50 | position: absolute; 51 | width: 0px; 52 | left: 0px; 53 | opacity: 0; 54 | } 55 | 56 | .header-button { 57 | background-color: #216490; 58 | padding: 5px 10px 5px 10px; 59 | color: white; 60 | cursor: pointer; 61 | border-radius: 50px; 62 | border: 0px; 63 | font-size: 11px; 64 | margin: 1px; 65 | line-height: 15px; 66 | } 67 | 68 | .play-pause-button { 69 | width: 34px; 70 | height: 34px; 71 | font-size: 16px; 72 | margin-right: 10px; 73 | } 74 | 75 | .play-pause-button-paused { 76 | background-color: #2ecc71; 77 | } 78 | 79 | body { 80 | margin: 0px; 81 | padding: 0px; 82 | background-color: #ecf0f1; 83 | } 84 | 85 | .main-body { 86 | display: flex; 87 | flex-direction: row; 88 | flex-wrap: wrap; 89 | } 90 | .left-col { 91 | min-width: min(250px, max(100px, 20%)); 92 | } 93 | .center-col { 94 | min-width: min(400px, 50%); 95 | flex: 3; 96 | padding-bottom: 100px; 97 | } 98 | .right-col { 99 | min-width: min(350px, max(250px, 30%)); 100 | flex: 1; 101 | } 102 | 103 | .help-container { 104 | display: flex; 105 | flex-direction: column; 106 | align-items: center; 107 | font-size: 16px; 108 | color: #7f8c8d; 109 | } 110 | 111 | .help-container img { 112 | display: block; 113 | width: 300px; 114 | margin-top: 100px; 115 | } 116 | .help-container span { 117 | display: block; 118 | margin-top: 100px; 119 | } 120 | 121 | .telem-container { 122 | display: flex; 123 | flex-direction: row; 124 | flex-wrap: wrap; 125 | justify-content: center; 126 | align-content: stretch; 127 | align-items: center; 128 | padding: 5px; 129 | } 130 | 131 | .telem-vue { 132 | flex: 1 1; 133 | align-self: auto; 134 | min-width: max(300px, 45%); 135 | box-shadow: 0px 0px 8px rgba(0,0,0,0.2); 136 | border-radius: 3px; 137 | margin: 5px; 138 | } 139 | 140 | .telem-vue-header { 141 | display: flex; 142 | flex-direction: row; 143 | justify-content: space-around; 144 | flex-wrap: wrap; 145 | } 146 | 147 | .telem-vue-title { 148 | display: block; 149 | font-size: 22px; 150 | text-align: center; 151 | } 152 | 153 | .var-container { 154 | display: flex; 155 | flex-direction: column; 156 | } 157 | 158 | .var-vue { 159 | margin: 2px 0px 2px 5px; 160 | font-size: 13px; 161 | display: flex; 162 | flex-direction: row; 163 | justify-content: space-between; 164 | cursor: grab; 165 | overflow: hidden; 166 | } 167 | 168 | .var-vue-name { 169 | color: black; 170 | max-width: 200px; 171 | word-break: break-all; 172 | border: 1px solid #2ecc71; 173 | flex: 1; 174 | padding: 0px 0px 1px 3px; 175 | border-radius: 5px 0px 0px 5px; 176 | } 177 | 178 | .var-vue-value { 179 | background-color: #2ecc71; 180 | padding: 1px 0px 1px 0px; 181 | color: white; 182 | width: 60px; 183 | text-align: center; 184 | display: flex; 185 | justify-content: center; 186 | align-items: center; 187 | border-radius: 0px 5px 5px 0px; 188 | } 189 | 190 | .cmd-container { 191 | display: flex; 192 | flex-direction: column; 193 | } 194 | 195 | .cmd-container-title { 196 | font-size: 18px; 197 | margin-top: 10px; 198 | } 199 | 200 | .cmd-vue { 201 | padding-top: 10px; 202 | } 203 | 204 | .log-container { 205 | display: flex; 206 | flex-direction: column-reverse; 207 | background-color: #2c3e50; 208 | font-size: 12px; 209 | height: 400px; 210 | min-width: 90%; 211 | overflow: auto; 212 | margin-top: 1em; 213 | padding-left: 5px; 214 | padding-bottom: 5px; 215 | } 216 | 217 | .log-vue { 218 | display: block; 219 | color: #ecf0f1; 220 | cursor: pointer; 221 | } 222 | .log-vue-selected { 223 | background-color: #27ae60; 224 | } 225 | 226 | .left-rounded-btn { 227 | border-radius: 50%; 228 | padding-right: 0px; 229 | display: block; 230 | margin-left: -20px; 231 | width: 40px; 232 | height: 40px; 233 | background-color: #2980b9; 234 | font-weight: bold; 235 | color: white; 236 | border: 0px; 237 | margin-top: 5px; 238 | } 239 | 240 | .right-rounded-btn { 241 | border-radius: 50%; 242 | padding-left: 0px; 243 | display: block; 244 | margin-right: -20px; 245 | width: 40px; 246 | height: 40px; 247 | background-color: #2980b9; 248 | font-weight: bold; 249 | color: white; 250 | border: 0px; 251 | margin-top: 5px; 252 | } 253 | 254 | .left-reduce-btn { 255 | width: 60px; 256 | margin-top: 0; 257 | padding: 2px; 258 | background-color: #2980b9; 259 | font-weight: bold; 260 | color: white; 261 | border: 0px; 262 | border-radius: 0 0 5px 0; 263 | } 264 | 265 | .right-reduce-btn { 266 | width: 60px; 267 | margin-top: 0; 268 | padding: 2px; 269 | background-color: #2980b9; 270 | font-weight: bold; 271 | color: white; 272 | border: 0px; 273 | border-radius: 0 0 0 5px; 274 | align-self: flex-end; 275 | } 276 | 277 | .send-text-container { 278 | display: flex; 279 | flex-direction: row; 280 | } 281 | 282 | 283 | .widget-container { 284 | padding: 5px; 285 | display:grid; 286 | grid-template-columns: repeat(12,calc(100% / 12)); 287 | grid-auto-rows: 50px; 288 | } 289 | 290 | .widget-vue { 291 | flex: 1 1; 292 | align-self: auto; 293 | min-width: max(150px, 45%); 294 | box-shadow: 0px 0px 8px rgba(0,0,0,0.15); 295 | border-radius: 3px; 296 | margin: 3px; 297 | border: 1px solid transparent; 298 | } 299 | 300 | .widget-drag-over { 301 | border: 1px solid #009aff; 302 | } 303 | 304 | .widget-vue-header { 305 | font-size: 12px; 306 | padding: 5px; 307 | display: flex; 308 | flex-direction: row; 309 | justify-content: space-around; 310 | } 311 | 312 | .serie-header-container { 313 | flex: 1; 314 | display: flex; 315 | flex-direction: row; 316 | justify-content: space-around; 317 | } 318 | .serie-header { 319 | display: flex; 320 | flex-direction: column; 321 | align-items: center; 322 | } 323 | 324 | .serie-color { 325 | display: inline-block; 326 | border-width: 2px; 327 | border-style: solid; 328 | margin-right: 3px; 329 | } 330 | 331 | .serie-name { 332 | color: black; 333 | } 334 | 335 | .serie-value { 336 | margin-left: 3px; 337 | border-radius: 3px; 338 | color: black; 339 | padding: 0px 4px; 340 | } 341 | .serie-header-small { 342 | font-size: 8px; 343 | } 344 | 345 | .serie-header-small .serie-value { 346 | margin-left: 0px; 347 | padding: 0px 1px; 348 | } 349 | 350 | .widget-option-container { 351 | display: flex; 352 | flex-direction: row; 353 | } 354 | 355 | .widget-option{ 356 | color: #bdc3c7; 357 | margin-left: 4px; 358 | margin-right: 4px; 359 | cursor: pointer; 360 | } 361 | 362 | .new-chart-drop-zone { 363 | margin: 5px; 364 | border: 2px dashed #3498db; 365 | border-radius: 5px; 366 | padding: 5px; 367 | color: #3498db; 368 | } 369 | 370 | .serie-name-container { 371 | display: flex; 372 | flex-direction: row; 373 | align-items: center; 374 | } 375 | 376 | .serie-stat-container { 377 | display: flex; 378 | flex-direction: row; 379 | align-items: center; 380 | flex-wrap: wrap; 381 | } 382 | 383 | .serie-stat { 384 | display: flex; 385 | flex-direction: column; 386 | margin: 1px 1px 1px 2px; 387 | line-height: 0.7em; 388 | } 389 | 390 | .serie-stat-value { 391 | font-size: 11px; 392 | color: #2c3e50; 393 | } 394 | .serie-stat-name { 395 | font-size: 9px; 396 | color: #7f8c8d; 397 | } 398 | 399 | 400 | 401 | .connection-container { 402 | display: flex; 403 | flex-direction: row; 404 | height: 100%; 405 | flex-wrap: wrap; 406 | } 407 | 408 | .connection { 409 | display: flex; 410 | flex-direction: column; 411 | margin: 2px 2px 2px 5px; 412 | } 413 | 414 | .connection-header { 415 | font-size: 11px; 416 | background-color: #216490; 417 | padding: 0px 5px; 418 | border-radius: 3px 3px 0px 0px; 419 | display: flex; 420 | flex-direction: row; 421 | justify-content: space-between; 422 | } 423 | 424 | .connection-name { 425 | 426 | } 427 | 428 | .connection-button-container { 429 | cursor: pointer; 430 | } 431 | 432 | .connection-button { 433 | margin-left: 10px; 434 | } 435 | 436 | .connection-input-container { 437 | display: flex; 438 | flex-direction: row; 439 | flex-wrap: wrap; 440 | background-color: #2164909e; 441 | border-radius: 0px 0px 5px 5px; 442 | overflow: hidden; 443 | } 444 | 445 | .connection-input { 446 | display: flex; 447 | flex-direction: column; 448 | font-size: 12px; 449 | padding-left: 5px; 450 | padding-right: 5px; 451 | border-width: 2px 2px 2px 2px; 452 | border-color: #216490; 453 | border-style: solid; 454 | } 455 | 456 | .connection-input-header { 457 | display: flex; 458 | flex-direction: row; 459 | justify-content: space-between; 460 | } 461 | 462 | .connection-input-name-container { 463 | display: flex; 464 | flex-direction: row; 465 | } 466 | 467 | .connection-input-name { 468 | font-size: 12px; 469 | } 470 | 471 | .connection-input-status-not-connected { 472 | background-color: #e74c3c; 473 | padding: 1px 5px 1px 5px; 474 | color: white; 475 | font-size: 11px; 476 | border-radius: 50px; 477 | } 478 | 479 | .connection-serial-input { 480 | display: flex; 481 | flex-direction: row; 482 | } 483 | 484 | .connection-serial-input-params { 485 | display: flex; 486 | flex-direction: row; 487 | } 488 | 489 | .connection-serial-input-params > select { 490 | font-size: 11px; 491 | } 492 | 493 | .connection-serial-buttons { 494 | display: flex; 495 | flex-direction: row; 496 | } 497 | 498 | .connection-serial-buttons > button { 499 | padding: 0px 5px; 500 | margin-left: 2px; 501 | } 502 | 503 | .connection-new-container { 504 | display: flex; 505 | flex-direction: row; 506 | font-size: 12px; 507 | margin-left: 10px; 508 | align-items: center; 509 | justify-content: center; 510 | } 511 | 512 | .connection-new-plus-button { 513 | background-color: #216490; 514 | color: white; 515 | border: 0; 516 | height: 30px; 517 | width: 30px; 518 | border-radius: 15px; 519 | } 520 | 521 | 522 | .pre-defined-connection-create-button { 523 | background-color: #216490; 524 | color: white; 525 | border: 0; 526 | height: 30px; 527 | border-radius: 15px; 528 | margin-right: 5px; 529 | } 530 | 531 | .flex-row { 532 | display: flex; 533 | flex-direction: row; 534 | } 535 | 536 | .flex-col { 537 | display: flex; 538 | flex-direction: column; 539 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teleplot", 3 | "description": "Teleplot - Ridiculously-simple telemetry viewer.", 4 | "version": "1.0.8", 5 | "publisher": "alexnesnes", 6 | "private": true, 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/nesnes/teleplot-vscode" 11 | }, 12 | "engines": { 13 | "vscode": "^1.47.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "onStartupFinished", 20 | "onCommand:teleplot.start", 21 | "onWebviewPanel:teleplot" 22 | ], 23 | "main": "./out/extension.js", 24 | "icon": "media/images/logo-color.png", 25 | "contributes": { 26 | "commands": [ 27 | { 28 | "command": "teleplot.start", 29 | "title": "Start teleplot session", 30 | "category": "teleplot", 31 | "icon": { 32 | "light": "media/images/logo-color.svg", 33 | "dark": "media/images/logo-color.svg" 34 | } 35 | } 36 | ] 37 | }, 38 | "scripts": { 39 | "vscode:prepublish": "npm run compile", 40 | "compile": "tsc -p ./", 41 | "rebuild:native-modules": "node ./node_modules/electron-rebuild/lib/cli.js --version 1.4.13", 42 | "lint": "eslint . --ext .ts,.tsx", 43 | "watch": "tsc -w -p ./" 44 | }, 45 | "dependencies": { 46 | "dgram": "^1.0.1", 47 | "node-usb-native": "^0.0.20", 48 | "serialport": "^10.4.0", 49 | "vsce": "^2.9.2" 50 | }, 51 | "devDependencies": { 52 | "@types/node": "^12.12.0", 53 | "@types/vscode": "^1.47.0", 54 | "@types/vscode-webview": "^1.57.0", 55 | "@typescript-eslint/eslint-plugin": "^4.16.0", 56 | "@typescript-eslint/parser": "^4.16.0", 57 | "electron-prebuilt": "1.4.13", 58 | "electron-rebuild": "^3.2.3", 59 | "eslint": "^7.21.0", 60 | "typescript": "^4.4.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesnes/teleplot-vscode/c51d9086e21503669bae0d0ceaf61d84a92680df/preview.png -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { ReadlineParser } from 'serialport'; 5 | const { SerialPort } = require('serialport')//require('node-usb-native'); 6 | const Readline = require('@serialport/parser-readline') 7 | 8 | const UDP_PORT = 47269; 9 | const CMD_UDP_PORT = 47268; 10 | 11 | const udp = require('dgram'); 12 | 13 | var serials : any = {}; 14 | var udpServer : any = null; 15 | var currentPanel:vscode.WebviewPanel; 16 | var _disposables: vscode.Disposable[] = []; 17 | var statusBarIcon:any; 18 | 19 | export function activate(context: vscode.ExtensionContext) { 20 | context.subscriptions.push( 21 | vscode.commands.registerCommand('teleplot.start', () => { 22 | startTeleplotServer(); 23 | 24 | const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; 25 | // If we already have a panel, show it. 26 | if (currentPanel) { 27 | currentPanel.reveal(column); 28 | return; 29 | } 30 | 31 | // Otherwise, create a new panel. 32 | const panel = vscode.window.createWebviewPanel('teleplot', 'Teleplot', column || vscode.ViewColumn.One, 33 | { 34 | enableScripts: true, 35 | localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))], 36 | retainContextWhenHidden: true, 37 | enableCommandUris: true 38 | } 39 | ); 40 | currentPanel = panel; 41 | 42 | fs.readFile(path.join(context.extensionPath, 'media', 'index.html') ,(err,data) => { 43 | if(err) {console.error(err)} 44 | let rawHTML = data.toString(); 45 | let replaceURI = (source:string, placeholder:string, path:any)=>{ 46 | const stylePath = vscode.Uri.joinPath(context.extensionUri, ...path); 47 | const styleUri = panel.webview.asWebviewUri(stylePath); 48 | return (source as any).replaceAll(placeholder, styleUri.toString()); 49 | } 50 | rawHTML = replaceURI(rawHTML, "{[styleURI]}", ['media', 'style.css']); 51 | rawHTML = replaceURI(rawHTML, "{[scriptURI]}", ['media', 'main.js']); 52 | rawHTML = replaceURI(rawHTML, "{[uPlotStyleURI]}", ['media', 'lib', 'uPlot.min.css']); 53 | rawHTML = replaceURI(rawHTML, "{[uPlotScriptURI]}", ['media', 'lib', 'uPlot.iife.min.js']); 54 | rawHTML = replaceURI(rawHTML, "{[uPlotComponentScriptURI]}", ['media', 'lib', 'uplot-component.js']); 55 | rawHTML = replaceURI(rawHTML, "{[vueScriptURI]}", ['media', 'lib', 'vue.js']); 56 | rawHTML = replaceURI(rawHTML, "{[logoMediaURI]}", ['media', 'images', 'logo-color.svg']); 57 | panel.webview.html = rawHTML; 58 | }); 59 | 60 | panel.onDidDispose(() => { 61 | if(udpServer) { 62 | udpServer.close(); 63 | udpServer = null; 64 | } 65 | while(_disposables.length) { 66 | const x = _disposables.pop(); 67 | if(x) x.dispose(); 68 | } 69 | _disposables.length = 0; 70 | for(let s in serials){ 71 | serials[s].close(); 72 | serials[s] = null; 73 | } 74 | (currentPanel as any) = null; 75 | }, null, _disposables); 76 | 77 | panel.webview.onDidReceiveMessage( message => { 78 | if("data" in message) { 79 | var udpClient = udp.createSocket('udp4'); 80 | udpClient.send(message.data, 0, message.data.length, CMD_UDP_PORT, ()=> { 81 | udpClient.close(); 82 | }); 83 | } 84 | else if("cmd" in message) { 85 | runCmd(message); 86 | } 87 | }, null, _disposables); 88 | }) 89 | ); 90 | 91 | statusBarIcon = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); 92 | statusBarIcon.command = 'teleplot.start'; 93 | statusBarIcon.text = "$(graph-line) Teleplot" 94 | context.subscriptions.push(statusBarIcon); 95 | statusBarIcon.show(); 96 | } 97 | 98 | function startTeleplotServer(){ 99 | // Setup UDP server 100 | udpServer = udp.createSocket('udp4'); 101 | udpServer.bind(UDP_PORT); 102 | // Relay UDP packets to webview 103 | udpServer.on('message',function(msg:any,info:any){ 104 | currentPanel.webview.postMessage({data: msg.toString(), fromSerial:false, timestamp: new Date().getTime()}); 105 | }); 106 | } 107 | 108 | var dataBuffer = ""; 109 | function runCmd(msg:any){ 110 | let id = ("id" in msg)?msg.id:""; 111 | if(msg.cmd == "listSerialPorts"){ 112 | SerialPort.list().then((ports:any) => { 113 | currentPanel.webview.postMessage({id, cmd: "serialPortList", list: ports}); 114 | }); 115 | } 116 | else if(msg.cmd == "connectSerialPort"){ 117 | if(serials[id]) { //Already exists 118 | serials[id].close(); 119 | delete serials[id]; 120 | } 121 | serials[id] = new SerialPort({baudRate: msg.baud, path: msg.port}, function(err: any) { 122 | if(err) { 123 | console.log("erroror"); 124 | currentPanel.webview.postMessage({id, cmd: "serialPortError", port: msg.port, baud: msg.baud}); 125 | } 126 | else { 127 | console.log("open"); 128 | currentPanel.webview.postMessage({id, cmd: "serialPortConnect", port: msg.port, baud: msg.baud}); 129 | } 130 | }) 131 | 132 | const parser = serials[id].pipe(new ReadlineParser({ delimiter: '\n' })); 133 | parser.on('data', function(data:any) { 134 | currentPanel.webview.postMessage({id, data: data.toString(), fromSerial:true, timestamp: new Date().getTime()}); 135 | }) 136 | serials[id].on('close', function(err:any) { 137 | currentPanel.webview.postMessage({id, cmd: "serialPortDisconnect"}); 138 | }) 139 | } 140 | else if(msg.cmd == "sendToSerial"){ 141 | serials[id].write(msg.text); 142 | } 143 | else if(msg.cmd == "disconnectSerialPort"){ 144 | serials[id].close(); 145 | delete serials[id]; 146 | } 147 | else if(msg.cmd == "saveFile"){ 148 | try { 149 | exportDataWithConfirmation(path.join(msg.file.name), { JSON: ["json"] }, msg.file.content); 150 | } catch (error) { 151 | void vscode.window.showErrorMessage("Couldn't write file: " + error); 152 | } 153 | } 154 | } 155 | 156 | function exportDataWithConfirmation(fileName: string, filters: { [name: string]: string[] }, data: string): void { 157 | void vscode.window.showSaveDialog({ 158 | defaultUri: vscode.Uri.file(fileName), 159 | filters, 160 | }).then((uri: vscode.Uri | undefined) => { 161 | if (uri) { 162 | const value = uri.fsPath; 163 | fs.writeFile(value, data, (error:any) => { 164 | if (error) { 165 | void vscode.window.showErrorMessage("Could not write to file: " + value + ": " + error.message); 166 | } else { 167 | void vscode.window.showInformationMessage("Saved " + value ); 168 | } 169 | }); 170 | } 171 | }); 172 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "src" 10 | }, 11 | "exclude": ["node_modules", ".vscode-test"] 12 | } 13 | --------------------------------------------------------------------------------