crosspad-mcp-server 9.1.1 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/config.d.ts +7 -0
  2. package/dist/config.js +15 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.js +125 -25
  5. package/dist/index.js.map +1 -1
  6. package/dist/tools/stm-build.d.ts +23 -0
  7. package/dist/tools/stm-build.js +78 -0
  8. package/dist/tools/stm-build.js.map +1 -0
  9. package/dist/tools/stm-flash.d.ts +36 -0
  10. package/dist/tools/stm-flash.js +112 -0
  11. package/dist/tools/stm-flash.js.map +1 -0
  12. package/dist/tools/trace-session.d.ts +12 -0
  13. package/dist/tools/trace-session.js +69 -0
  14. package/dist/tools/trace-session.js.map +1 -1
  15. package/dist/tools/trace-webui.d.ts +14 -7
  16. package/dist/tools/trace-webui.js +77 -33
  17. package/dist/tools/trace-webui.js.map +1 -1
  18. package/dist/tools/trace-write.d.ts +9 -0
  19. package/dist/tools/trace-write.js +32 -0
  20. package/dist/tools/trace-write.js.map +1 -0
  21. package/dist/utils/userConfig.d.ts +9 -0
  22. package/dist/utils/userConfig.js.map +1 -1
  23. package/package.json +2 -1
  24. package/skills/swd-tracer/SKILL.md +48 -0
  25. package/tracer/PROTOCOL.md +69 -0
  26. package/tracer/swd_tracer.py +449 -12
  27. package/tracer/ui/config.js +40 -0
  28. package/tracer/ui/connection.js +170 -0
  29. package/tracer/ui/index.html +50 -798
  30. package/tracer/ui/main.js +30 -0
  31. package/tracer/ui/plot.js +337 -0
  32. package/tracer/ui/signals.js +39 -0
  33. package/tracer/ui/state.js +57 -0
  34. package/tracer/ui/stats.js +52 -0
  35. package/tracer/ui/style.css +97 -0
  36. package/tracer/ui/symbols.js +80 -0
  37. package/tracer/ui/toolbar.js +95 -0
  38. package/tracer/ui/util.js +62 -0
  39. package/tracer/ui/watchlist.js +179 -0
  40. package/vscode-extension/README.md +32 -0
  41. package/vscode-extension/extension.js +56 -0
  42. package/vscode-extension/package.json +14 -0
@@ -0,0 +1,30 @@
1
+ /* Entry point: wire the modules, paint initial state, open the socket, start the
2
+ render loop. Imported as a module from index.html. */
3
+
4
+ import { initPlot } from "./plot.js";
5
+ import { connect, setConn, setBanner } from "./connection.js";
6
+ import { initWatchlist } from "./watchlist.js";
7
+ import { initConfig } from "./config.js";
8
+ import { initToolbar } from "./toolbar.js";
9
+ import { loadSymbols } from "./symbols.js";
10
+
11
+ /* Collapsible side panels. */
12
+ function initPanels(){
13
+ document.querySelectorAll(".panel h3[data-tgl]").forEach(h=>{
14
+ h.onclick=()=>{
15
+ const p=h.parentElement; p.classList.toggle("collapsed");
16
+ const arr=h.querySelector(".arrow"); if(arr) arr.textContent=p.classList.contains("collapsed")?"▸":"▾";
17
+ };
18
+ });
19
+ }
20
+
21
+ initConfig();
22
+ initWatchlist();
23
+ initToolbar();
24
+ initPanels();
25
+ initPlot(); // grabs the canvas, wires input, starts the render loop
26
+
27
+ setConn("reconnecting"); // initial indicator paint before the first open
28
+ setBanner("connecting…","idle");
29
+ connect();
30
+ loadSymbols(); // populate autocomplete (graceful if unavailable)
@@ -0,0 +1,337 @@
1
+ /* Canvas rendering + plot interaction (zoom/pan/hover). Owns the render loop,
2
+ the visible-range math, signal paths, lane mode, the crosshair overlay, and
3
+ the grid. View/hover overrides live on the shared `state` object. */
4
+
5
+ import { state, sigs, cfg } from "./state.js";
6
+ import { fmt, nearestSample } from "./util.js";
7
+ import { updateStats } from "./stats.js";
8
+ import { setWindow } from "./config.js";
9
+
10
+ let cv=null, ctx=null;
11
+
12
+ /* Render-style knobs, swapped up during PNG export (thicker lines, lighter grid,
13
+ bigger fonts) and restored after. S scales fonts; the export canvas is also
14
+ drawn at a higher pixel size for a sharp, legible image. */
15
+ let LW=1.25; // signal line width
16
+ let GRID="#1c1c1c"; // grid line color
17
+ let S=1; // font scale
18
+
19
+ /* Resize the backing store to the element's CSS size. */
20
+ export function fit(){ if(cv){ cv.width=cv.clientWidth; cv.height=cv.clientHeight; } }
21
+
22
+ /* ---------- visible time range ---------- */
23
+ function dataBounds(){
24
+ let lo=Infinity,hi=-Infinity;
25
+ for(const[,s]of sigs){const d=s.data;if(d.length){if(d[0].t<lo)lo=d[0].t;if(d[d.length-1].t>hi)hi=d[d.length-1].t;}}
26
+ return [lo,hi];
27
+ }
28
+ export function curRange(){
29
+ const [lo,hi]=dataBounds();
30
+ if(!isFinite(lo))return[0,1];
31
+ if(state.viewT0!=null)return[state.viewT0,state.viewT1];
32
+ if(cfg.full)return[lo,hi>lo?hi:lo+1];
33
+ return[Math.max(lo,hi-cfg.windowSec),hi];
34
+ }
35
+
36
+ /* Shared-mode Y bounds: manual override (Shift+wheel) wins, else auto-fit. */
37
+ export function sharedYBounds(t0,t1){
38
+ if(state.viewY0!=null&&state.viewY1!=null)return[state.viewY0,state.viewY1];
39
+ let lo=Infinity,hi=-Infinity;
40
+ for(const[,s]of sigs){if(!s.on)continue;
41
+ for(const p of s.data){if(p.t<t0||p.t>t1)continue;if(p.v<lo)lo=p.v;if(p.v>hi)hi=p.v;}}
42
+ if(!isFinite(lo)){lo=0;hi=1;}
43
+ if(hi===lo)hi=lo+1;
44
+ return[lo,hi];
45
+ }
46
+
47
+ /* Value bounds across all on-signals' full history (for the Y pan/zoom clamp). */
48
+ function valueBounds(){
49
+ let lo=Infinity,hi=-Infinity;
50
+ for(const[,s]of sigs){if(!s.on)continue;for(const p of s.data){if(p.v<lo)lo=p.v;if(p.v>hi)hi=p.v;}}
51
+ return[lo,hi];
52
+ }
53
+
54
+ /* Clamp a manual [a,b] override against data [lo,hi]. A small overscroll margin
55
+ (a fraction of the view span) is allowed past each end so the user SEES a bit
56
+ of empty space at the edge — a clear "nothing here" instead of the pan
57
+ appearing to lock for no reason. Returns clamped [a,b] or null. */
58
+ const OVERSCROLL=0.08;
59
+ function clampSpan(a,b,lo,hi){
60
+ if(a==null||!isFinite(lo)||hi<=lo)return null;
61
+ const span=b-a; if(!(span>0))return null;
62
+ const ov=span*OVERSCROLL;
63
+ const LO=lo-ov, HI=hi+ov; // edges may sit one margin past the data
64
+ if(span>=HI-LO)return[LO,HI]; // zoomed out past data+margins → snap
65
+ if(a<LO){ a=LO; b=LO+span; }
66
+ if(b>HI){ b=HI; a=HI-span; }
67
+ return[a,b];
68
+ }
69
+
70
+ /* Keep the manual time view inside the data's time extent — you can't scroll the
71
+ signal off-screen or pan into empty space beyond where it reaches. */
72
+ function clampViewTime(){
73
+ const [lo,hi]=dataBounds();
74
+ const c=clampSpan(state.viewT0,state.viewT1,lo,hi);
75
+ if(c){ state.viewT0=c[0]; state.viewT1=c[1]; }
76
+ }
77
+ /* Same for the shared-mode Y override (Shift+wheel). */
78
+ function clampViewY(){
79
+ const [lo,hi]=valueBounds();
80
+ const c=clampSpan(state.viewY0,state.viewY1,lo,hi);
81
+ if(c){ state.viewY0=c[0]; state.viewY1=c[1]; }
82
+ }
83
+
84
+ /* Stroke one signal's visible polyline. step=true → zero-order hold (logic look). */
85
+ function plotPath(s,X,Y,t0,t1,step){
86
+ ctx.strokeStyle=s.color;ctx.lineWidth=LW;ctx.beginPath();
87
+ const d=s.data;let first=true,py=0;
88
+ for(let i=0;i<d.length;i++){
89
+ const p=d[i]; if(p.t<t0||p.t>t1)continue;
90
+ const x=X(p.t),y=Y(p.v);
91
+ if(first){ctx.moveTo(x,y);first=false;}
92
+ else{ if(step)ctx.lineTo(x,py); ctx.lineTo(x,y); }
93
+ py=y;
94
+ }
95
+ ctx.stroke();
96
+ }
97
+
98
+ /* Lane mode: stack each visible signal in its own auto-scaled band with a colored
99
+ label. DISCRETE signals (all-integer, small range — bits / flags) render as a
100
+ zero-order-hold staircase (the logic-analyzer look); ANALOG signals render with
101
+ straight interpolation like shared mode, so they don't look artificially blocky.
102
+ Returns plotted [{n,s,Y}]. */
103
+ function renderLanesPath(visible,t0,t1,X,W,H){
104
+ const plotted=[];const n=visible.length; if(!n)return plotted;
105
+ const laneH=H/n, m=6;
106
+ for(let i=0;i<n;i++){
107
+ const [name,s]=visible[i];
108
+ const top=i*laneH, bot=(i+1)*laneH;
109
+ if(i>0){ctx.strokeStyle="#1c1c1c";ctx.lineWidth=1;
110
+ ctx.beginPath();ctx.moveTo(0,top+0.5);ctx.lineTo(W,top+0.5);ctx.stroke();}
111
+ let lo=Infinity,hi=-Infinity,allInt=true;
112
+ for(const p of s.data){if(p.t<t0||p.t>t1)continue;
113
+ if(p.v<lo)lo=p.v;if(p.v>hi)hi=p.v;
114
+ if(allInt&&!Number.isInteger(p.v))allInt=false;}
115
+ if(!isFinite(lo)){lo=0;hi=1;} if(hi===lo)hi=lo+1;
116
+ const span=(hi-lo)||1;
117
+ const Y=(v)=>bot-m-((v-lo)/span)*(laneH-2*m);
118
+ const stepMode = allInt && (hi-lo)<=16; // discrete → staircase; analog → linear
119
+ plotPath(s,X,Y,t0,t1,stepMode);
120
+ plotted.push({n:name,s,Y});
121
+ // top-left: lane max value (muted) + signal name (colored); bottom-left: min.
122
+ ctx.font=`${10*S}px monospace`;
123
+ const hiTxt=fmt(hi), loTxt=fmt(lo);
124
+ const hiW=ctx.measureText(hiTxt).width, nameW=ctx.measureText(name).width;
125
+ ctx.fillStyle="#000a";ctx.fillRect(2*S,top+2*S,hiW+6*S+nameW+6*S,12*S);
126
+ ctx.fillStyle="#888";ctx.fillText(hiTxt,4*S,top+11*S);
127
+ ctx.fillStyle=s.color;ctx.fillText(name,4*S+hiW+6*S,top+11*S);
128
+ const loW=ctx.measureText(loTxt).width;
129
+ ctx.fillStyle="#000a";ctx.fillRect(2*S,bot-13*S,loW+4*S,12*S);
130
+ ctx.fillStyle="#888";ctx.fillText(loTxt,4*S,bot-4*S);
131
+ }
132
+ return plotted;
133
+ }
134
+
135
+ /* Crosshair + on-canvas legend overlay (top-right). The legend always identifies
136
+ colors; while hovering it also shows each signal's value at the cursor time,
137
+ draws a dot on each curve, and a dashed vertical line + time label. */
138
+ function drawOverlay(plotted,t0,t1,X,W,H){
139
+ const span=(t1-t0)||1;
140
+ const hx=state.hoverX;
141
+ const hover=hx!=null&&hx>=0&&hx<=W;
142
+ const hoverT=hover?t0+(hx/W)*span:null;
143
+ if(hover){
144
+ ctx.save();
145
+ ctx.strokeStyle="#888";ctx.lineWidth=1;ctx.setLineDash([4,3]);
146
+ ctx.beginPath();ctx.moveTo(hx+0.5,0);ctx.lineTo(hx+0.5,H);ctx.stroke();
147
+ ctx.setLineDash([]);
148
+ const tl=hoverT.toFixed(3)+"s"; ctx.font="10px monospace";
149
+ const tw=ctx.measureText(tl).width+6; let lx=hx+4; if(lx+tw>W)lx=hx-4-tw;
150
+ ctx.fillStyle="#000c";ctx.fillRect(lx,2,tw,13);
151
+ ctx.fillStyle="#bbb";ctx.fillText(tl,lx+3,12);
152
+ ctx.restore();
153
+ }
154
+ drawLegend(plotted,X,W,hover,hoverT);
155
+ }
156
+
157
+ /* On-canvas legend box (top-right): color + name, plus the value at the cursor
158
+ while hovering (also draws a dot on each curve). Shared by the live overlay
159
+ and the PNG export (always-on, no crosshair). */
160
+ function drawLegend(plotted,X,W,hover,hoverT){
161
+ const rows=[];
162
+ for(const pl of plotted){
163
+ let val=null;
164
+ if(hover){
165
+ const p=nearestSample(pl.s.data,hoverT);
166
+ if(p){val=p.v; const x=X(p.t),y=pl.Y(p.v);
167
+ if(isFinite(x)&&isFinite(y)){
168
+ ctx.fillStyle=pl.s.color;ctx.beginPath();ctx.arc(x,y,3*S,0,Math.PI*2);ctx.fill();
169
+ ctx.strokeStyle="#000";ctx.lineWidth=1;ctx.stroke();
170
+ }}
171
+ }
172
+ rows.push({name:pl.n,color:pl.s.color,val});
173
+ }
174
+ if(!rows.length)return;
175
+ ctx.save();ctx.font=`${11*S}px monospace`;
176
+ let maxName=0;for(const r of rows)maxName=Math.max(maxName,ctx.measureText(r.name).width);
177
+ let maxVal=0;if(hover)for(const r of rows)maxVal=Math.max(maxVal,ctx.measureText(fmt(r.val)).width);
178
+ const pad=6*S,sw=9*S,gap=8*S,lh=14*S;
179
+ const boxW=pad+sw+5*S+maxName+(hover?gap+maxVal:0)+pad;
180
+ const boxH=pad+rows.length*lh+pad-2*S;
181
+ const bx=W-boxW-6*S,by=6*S;
182
+ ctx.fillStyle="#000a";ctx.fillRect(bx,by,boxW,boxH);
183
+ ctx.strokeStyle="#2a2a2a";ctx.strokeRect(bx+0.5,by+0.5,boxW,boxH);
184
+ let y=by+pad+9*S;
185
+ for(const r of rows){
186
+ ctx.fillStyle=r.color;ctx.fillRect(bx+pad,y-8*S,sw,sw);
187
+ ctx.fillStyle="#ccc";ctx.fillText(r.name,bx+pad+sw+5*S,y);
188
+ if(hover){ctx.fillStyle="#fff";ctx.fillText(fmt(r.val),bx+pad+sw+5*S+maxName+gap,y);}
189
+ y+=lh;
190
+ }
191
+ ctx.restore();
192
+ }
193
+
194
+ /* ---------- grid / axes ---------- */
195
+ function drawGrid(t0,t1,W,H,ylo,yhi){
196
+ ctx.strokeStyle=GRID;ctx.lineWidth=1;ctx.fillStyle="#888";ctx.font=`${10*S}px monospace`;
197
+ for(let i=0;i<=6;i++){
198
+ const x=W*i/6; ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();
199
+ const tt=t0+(t1-t0)*i/6;
200
+ ctx.fillText(tt.toFixed(2)+"s",Math.min(x+2*S,W-40*S),H-2*S);
201
+ }
202
+ if(cfg.yMode==="shared"){
203
+ for(let i=0;i<=4;i++){
204
+ const y=H*i/4; ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();
205
+ const vv=yhi-(yhi-ylo)*i/4;
206
+ ctx.fillText(fmt(vv),2*S,Math.max(10*S,y+10*S));
207
+ }
208
+ }
209
+ }
210
+
211
+ /* Paint grid + every visible signal for the current view into the active canvas
212
+ (cv/ctx). Returns the `plotted` list ({n,s,Y}) for overlays. Shared by the
213
+ live render loop and the PNG exporter. */
214
+ function paintSignals(t0,t1,W,H){
215
+ const span=(t1-t0)||1;
216
+ const X=(t)=>(t-t0)/span*W;
217
+ const visible=[...sigs].filter(([,s])=>s.on);
218
+ if(cfg.yMode==="lanes"){
219
+ drawGrid(t0,t1,W,H,0,1);
220
+ return { X, plotted: renderLanesPath(visible,t0,t1,X,W,H) };
221
+ }
222
+ const [ylo,yhi]=sharedYBounds(t0,t1);
223
+ const yspan=(yhi-ylo)||1;
224
+ const sharedY=(v)=>H-(v-ylo)/yspan*H;
225
+ drawGrid(t0,t1,W,H,ylo,yhi);
226
+ const plotted=[];
227
+ for(const[n,s] of visible){
228
+ let Y=sharedY;
229
+ if(cfg.yMode==="norm"){
230
+ let smn=Infinity,smx=-Infinity;
231
+ for(const p of s.data){if(p.t<t0||p.t>t1)continue;if(p.v<smn)smn=p.v;if(p.v>smx)smx=p.v;}
232
+ if(!isFinite(smn)){smn=0;smx=1;}
233
+ const ss=(smx-smn)||1;
234
+ Y=(v)=>H-((v-smn)/ss)*(H-8)-4;
235
+ }
236
+ plotPath(s,X,Y,t0,t1,false);
237
+ plotted.push({n,s,Y});
238
+ }
239
+ return { X, plotted };
240
+ }
241
+
242
+ /* ---------- render loop ---------- */
243
+ function render(){
244
+ requestAnimationFrame(render);
245
+ if(!ctx)return;
246
+ ctx.clearRect(0,0,cv.width,cv.height);
247
+ const [t0,t1]=curRange();
248
+ const W=cv.width,H=cv.height;
249
+ const { X, plotted }=paintSignals(t0,t1,W,H);
250
+ drawOverlay(plotted,t0,t1,X,W,H);
251
+ updateStats(t0,t1);
252
+ }
253
+
254
+ /* ---------- PNG export ----------
255
+ Render a sharp, high-contrast, self-contained image to an offscreen canvas:
256
+ 2× pixel scale, thicker lines, lighter grid, an always-on legend (so the saved
257
+ image identifies its signals), and a title strip with the date, time window,
258
+ and Fs. Returns the offscreen canvas for toBlob. */
259
+ export function renderExport(fsText){
260
+ const SC=2; // pixel + font scale
261
+ const W=(cv.width||800)*SC, H=(cv.height||400)*SC;
262
+ const off=document.createElement("canvas"); off.width=W; off.height=H;
263
+ const offCtx=off.getContext("2d");
264
+ const sCv=cv,sCtx=ctx,sLW=LW,sGRID=GRID,sS=S;
265
+ cv=off; ctx=offCtx; LW=2.2*SC; GRID="#333"; S=SC;
266
+ try{
267
+ offCtx.fillStyle="#0c0c0c"; offCtx.fillRect(0,0,W,H);
268
+ const titleH=22*SC;
269
+ const [t0,t1]=curRange();
270
+ // plot below the title strip
271
+ offCtx.save(); offCtx.translate(0,titleH);
272
+ const { X, plotted }=paintSignals(t0,t1,W,H-titleH);
273
+ drawLegend(plotted,X,W,false,null); // always-on legend, no crosshair
274
+ offCtx.restore();
275
+ // title strip
276
+ offCtx.fillStyle="#141414"; offCtx.fillRect(0,0,W,titleH);
277
+ offCtx.strokeStyle="#2a2a2a"; offCtx.beginPath();offCtx.moveTo(0,titleH-0.5);offCtx.lineTo(W,titleH-0.5);offCtx.stroke();
278
+ offCtx.fillStyle="#bcd"; offCtx.font=`${12*SC}px monospace`; offCtx.textBaseline="middle";
279
+ const stamp=new Date().toLocaleString();
280
+ const title=`CrossPad SWD trace · ${stamp} · ${t0.toFixed(2)}–${t1.toFixed(2)}s · ${cfg.yMode}${fsText?` · ${fsText}`:""}`;
281
+ offCtx.fillText(title,8*SC,titleH/2);
282
+ offCtx.textBaseline="alphabetic";
283
+ } finally {
284
+ cv=sCv;ctx=sCtx;LW=sLW;GRID=sGRID;S=sS;
285
+ }
286
+ return off;
287
+ }
288
+
289
+ /* ---------- init: grab canvas, wire input, start the loop ----------
290
+ Both wheel axes are honored: deltaY zooms time (Shift+deltaY zooms shared Y),
291
+ deltaX (horizontal/tilt wheel) pans time. A 2-D scroll applies both. */
292
+ export function initPlot(){
293
+ cv=document.getElementById("cv");
294
+ ctx=cv.getContext("2d");
295
+ window.addEventListener("resize",fit);
296
+ fit();
297
+
298
+ cv.addEventListener("wheel",(e)=>{e.preventDefault();
299
+ const W=cv.width||1, H=cv.height||1;
300
+ const [t0,t1]=curRange();const span=(t1-t0)||1;
301
+ if(e.deltaY){
302
+ if(e.shiftKey && cfg.yMode==="shared"){
303
+ const [y0,y1]=sharedYBounds(t0,t1);const yspan=(y1-y0)||1;
304
+ const f=e.deltaY<0?0.8:1.25;
305
+ const cy=y1-(e.offsetY/H)*yspan;
306
+ state.viewY0=cy-(cy-y0)*f; state.viewY1=cy+(y1-cy)*f;
307
+ clampViewY();
308
+ }else{
309
+ const f=e.deltaY<0?0.8:1.25;
310
+ const cx=t0+span*(e.offsetX/W);
311
+ state.viewT0=cx-(cx-t0)*f; state.viewT1=cx+(t1-cx)*f;
312
+ clampViewTime();
313
+ setWindow(state.viewT1-state.viewT0); // keep the Trailing window field in sync
314
+ }
315
+ }
316
+ if(e.deltaX){
317
+ const dt=(e.deltaX/W)*span;
318
+ const [a,b]=(state.viewT0!=null)?[state.viewT0,state.viewT1]:[t0,t1];
319
+ state.viewT0=a+dt; state.viewT1=b+dt;
320
+ clampViewTime();
321
+ }
322
+ },{passive:false});
323
+
324
+ let drag=null;
325
+ cv.addEventListener("mousedown",(e)=>drag={x:e.offsetX,r:curRange()});
326
+ window.addEventListener("mouseup",()=>drag=null);
327
+ cv.addEventListener("mousemove",(e)=>{
328
+ state.hoverX=e.offsetX; state.hoverY=e.offsetY;
329
+ if(!drag)return;
330
+ const [t0,t1]=drag.r;const span=t1-t0;const dt=(e.offsetX-drag.x)/cv.width*span;
331
+ state.viewT0=t0-dt;state.viewT1=t1-dt;
332
+ clampViewTime();
333
+ });
334
+ cv.addEventListener("mouseleave",()=>{ state.hoverX=null; state.hoverY=null; });
335
+
336
+ render();
337
+ }
@@ -0,0 +1,39 @@
1
+ /* Local model of the watched signal set: reconcile to the server's set, and
2
+ append samples. The view is rebuilt via bus.rebuildList (assigned by the
3
+ watchlist module) so this stays free of DOM concerns. */
4
+
5
+ import { sigs, cfg, nextColor, bus } from "./state.js";
6
+ import { toast } from "./util.js";
7
+ import { getSuggestList, loadSymbols } from "./symbols.js";
8
+
9
+ let symbolsRetried = false;
10
+
11
+ /* hello.signals are plain name strings; signals frames are full descriptors. */
12
+ export function toDesc(n){ return (n&&typeof n==="object")?n:{name:String(n)}; }
13
+
14
+ /* Make the local sigs map match `descs` exactly: add new (stable color, on),
15
+ update fields, drop removed. `unresolved` (if any) is toasted so the user
16
+ knows a spec didn't land. */
17
+ export function reconcile(descs, unresolved){
18
+ const want=new Set(descs.map(d=>d.name));
19
+ for(const n of [...sigs.keys()]) if(!want.has(n)) sigs.delete(n);
20
+ for(const d of descs){
21
+ let s=sigs.get(d.name);
22
+ if(!s){ s={color:nextColor(),on:true,data:[]}; sigs.set(d.name,s); }
23
+ if(d.address!=null) s.address=d.address;
24
+ if(d.size!=null) s.size=d.size;
25
+ if(d.encoding!=null) s.encoding=d.encoding;
26
+ }
27
+ if(unresolved && unresolved.length) toast("unresolved: "+unresolved.join(", "));
28
+ bus.rebuildList();
29
+ // If autocomplete is still empty (symbols fetched before a session was active),
30
+ // retry once now that the server is clearly talking to a target.
31
+ if(!getSuggestList().length && !symbolsRetried){ symbolsRetried=true; loadSymbols(); }
32
+ }
33
+
34
+ export function push(n,t,v){
35
+ const s=sigs.get(n); if(!s)return;
36
+ s.data.push({t,v});
37
+ const cap=Math.max(10,cfg.maxSamples|0);
38
+ if(s.data.length>cap) s.data.shift();
39
+ }
@@ -0,0 +1,57 @@
1
+ /* Shared mutable state + small data singletons for the trace dashboard.
2
+ Reassignable scalars (paused / view overrides / hover) live on the `state`
3
+ object so any module can update them — ESM exports are read-only bindings for
4
+ importers, but object PROPERTIES are freely mutable, which is exactly what the
5
+ render loop, input handlers, and toolbar all need. */
6
+
7
+ export const state = {
8
+ paused: false,
9
+ // Manual view overrides; null = follow the live trailing window / auto-Y.
10
+ viewT0: null, viewT1: null, // time pan/zoom (and pause-freeze)
11
+ viewY0: null, viewY1: null, // shared-mode Y zoom (Shift+wheel)
12
+ // Crosshair: canvas-space mouse position while hovering (null = not hovering).
13
+ hoverX: null, hoverY: null,
14
+ };
15
+
16
+ /* Client-side config (mirrors the side-panel controls). */
17
+ export const cfg = { windowSec: 15, full: false, maxSamples: 100000, yMode: "shared" };
18
+
19
+ /* sigs: name -> {color,on,data:[{t,v}],address?,size?,encoding?} */
20
+ export const sigs = new Map();
21
+
22
+ /* ---------- palette + stable color assignment ----------
23
+ Colors rotate by index so a signal keeps its color across reconciles even as
24
+ others are added/removed. */
25
+ const palette = ["#4af","#fa4","#4f8","#f48","#af4","#8af","#fd4","#f84","#6df","#d6f","#9f6","#f96"];
26
+ let colorIdx = 0;
27
+ export function nextColor(){ return palette[(colorIdx++) % palette.length]; }
28
+
29
+ /* ---------- global Fs (PROTOCOL §10) ----------
30
+ Sample rate is a property of the whole trace, estimated once from the spacing
31
+ of incoming sample-frame timestamps (a short ring → smoothed rate). An
32
+ explicit `actual_fs`/`fs` from a frame is authoritative and wins. */
33
+ export const fsState = { stamps: [], reported: null, lastShown: "" };
34
+ const FS_RING = 64;
35
+ export function fsNoteSample(t){
36
+ if(typeof t!=="number"||!isFinite(t))return;
37
+ const r=fsState.stamps;
38
+ if(r.length && t<=r[r.length-1]) return; // ignore dup/out-of-order
39
+ r.push(t);
40
+ if(r.length>FS_RING) r.shift();
41
+ }
42
+ export function fsEstimate(){
43
+ if(fsState.reported!=null && isFinite(fsState.reported) && fsState.reported>0) return fsState.reported;
44
+ const r=fsState.stamps;
45
+ if(r.length<2) return null;
46
+ const dt=r[r.length-1]-r[0];
47
+ if(!(dt>0)) return null;
48
+ return (r.length-1)/dt;
49
+ }
50
+
51
+ /* ---------- UI hooks (decouple data/connection layers from the view) ----------
52
+ The view layer assigns these; data/connection layers call them. Keeps the
53
+ module graph acyclic (no connection→view import). */
54
+ export const bus = {
55
+ rebuildList: () => {}, // signal set changed → rebuild the Signals panel
56
+ onSymbols: () => {}, // /symbols (re)loaded → refresh an open dropdown
57
+ };
@@ -0,0 +1,52 @@
1
+ /* Per-signal value-domain stats over the visible window, plus the single global
2
+ Fs readout. VALUE-DOMAIN metrics only (cur/min/max/mean/p2p/rms/n). Sample
3
+ rate is a whole-trace property shown once in the top bar (PROTOCOL §10). */
4
+
5
+ import { sigs, fsState, fsEstimate } from "./state.js";
6
+ import { fmt, cssid } from "./util.js";
7
+
8
+ export function statsFor(s,t0,t1){
9
+ let n=0,sum=0,sq=0,mn=Infinity,mx=-Infinity,cur=null;
10
+ const d=s.data;
11
+ for(let i=0;i<d.length;i++){
12
+ const p=d[i];
13
+ if(p.t<t0||p.t>t1) continue;
14
+ n++; sum+=p.v; sq+=p.v*p.v;
15
+ if(p.v<mn)mn=p.v; if(p.v>mx)mx=p.v;
16
+ cur=p.v;
17
+ }
18
+ if(n===0){
19
+ cur=d.length?d[d.length-1].v:null;
20
+ return {n:0,cur,min:null,max:null,mean:null,p2p:null,rms:null};
21
+ }
22
+ const mean=sum/n;
23
+ const rms=Math.sqrt(sq/n);
24
+ return {n,cur,min:mn,max:mx,mean,p2p:mx-mn,rms};
25
+ }
26
+
27
+ let lastStatsAt=0;
28
+ /* Refresh per-row stats cells (#st-<id>) + the global Fs readout, throttled. */
29
+ export function updateStats(t0,t1){
30
+ const now=performance.now();
31
+ if(now-lastStatsAt<200)return; // ~5 Hz, cheap
32
+ lastStatsAt=now;
33
+ for(const [n,s] of sigs){
34
+ const cell=document.getElementById("st-"+cssid(n));
35
+ if(!cell)continue;
36
+ const st=statsFor(s,t0,t1);
37
+ cell.innerHTML=
38
+ `<span>cur <b>${fmt(st.cur)}</b></span>`+
39
+ `<span>min <b>${fmt(st.min)}</b></span>`+
40
+ `<span>max <b>${fmt(st.max)}</b></span>`+
41
+ `<span>mean <b>${fmt(st.mean)}</b></span>`+
42
+ `<span>p2p <b>${fmt(st.p2p)}</b></span>`+
43
+ `<span>rms <b>${fmt(st.rms)}</b></span>`+
44
+ `<span>n <b>${st.n}</b></span>`;
45
+ }
46
+ const fsEl=document.getElementById("fs");
47
+ if(fsEl){
48
+ const f=fsEstimate();
49
+ const txt="Fs "+(f!=null?fmt(f)+" Hz":"—");
50
+ if(txt!==fsState.lastShown){ fsEl.textContent=txt; fsState.lastShown=txt; }
51
+ }
52
+ }
@@ -0,0 +1,97 @@
1
+ *{box-sizing:border-box}
2
+ body{font:13px monospace;margin:0;background:#111;color:#ddd}
3
+ #wrap{display:flex;height:100vh}
4
+ #side{width:340px;min-width:340px;padding:8px;overflow:auto;border-right:1px solid #333;display:flex;flex-direction:column;gap:8px}
5
+ #main{flex:1;display:flex;flex-direction:column;min-width:0}
6
+ canvas{flex:1;background:#000;display:block;cursor:crosshair}
7
+
8
+ button{background:#222;color:#ddd;border:1px solid #444;padding:3px 8px;cursor:pointer;border-radius:3px}
9
+ button:hover{background:#2c2c2c}
10
+ button.active{background:#264;border-color:#5a5}
11
+ button:focus-visible{outline:1px solid #6cf}
12
+ input,select{background:#1a1a1a;color:#ddd;border:1px solid #444;padding:2px 4px;border-radius:3px;font:12px monospace}
13
+ input:focus,select:focus{outline:none;border-color:#48a}
14
+
15
+ #bar{position:relative;padding:4px;display:flex;gap:6px;border-bottom:1px solid #333;align-items:center;flex-wrap:wrap}
16
+ /* round "i" help button + its popover (replaces the canvas tooltip) */
17
+ button.i{width:20px;height:20px;padding:0;border-radius:50%;font-style:italic;font-weight:bold;line-height:1;color:#9ab}
18
+ button.i:hover{color:#cde;border-color:#48a}
19
+ .help{position:absolute;top:100%;right:6px;margin-top:4px;z-index:30;width:340px;
20
+ background:#15171b;border:1px solid #3a4250;border-radius:5px;padding:8px 10px;
21
+ box-shadow:0 6px 18px #000a;font-size:11px;color:#bcd;line-height:1.7}
22
+ .help[hidden]{display:none}
23
+ .help h4{margin:2px 0 3px;color:#778}
24
+ .help h4:first-child{margin-top:0}
25
+ #bar #fs{margin-left:auto;color:#6cf;font-weight:bold}
26
+ #bar #stat{color:#9c9}
27
+ #bar .sep{width:1px;align-self:stretch;background:#333;margin:0 2px}
28
+ #bar .grp{display:inline-flex;gap:4px;align-items:center}
29
+
30
+ /* kbd badge — used inline inside buttons/labels (shortcuts live ON the control
31
+ they trigger, not in a separate hint strip). */
32
+ kbd{background:#1b1b1b;border:1px solid #444;border-bottom-width:2px;border-radius:3px;padding:0 4px;font:10px monospace;color:#9ab;margin-left:2px}
33
+ button kbd{margin-left:4px}
34
+
35
+ h3,h4{margin:4px 0;font-weight:normal;color:#888;text-transform:uppercase;font-size:11px;letter-spacing:1px}
36
+ .panel{border:1px solid #2a2a2a;border-radius:4px;padding:6px;background:#161616}
37
+ .panel.collapsed .body{display:none}
38
+ .panel h3{cursor:pointer;user-select:none;display:flex;align-items:center;gap:4px}
39
+ .panel h3 .arrow{color:#666}
40
+ .row{display:flex;gap:4px;align-items:center;margin:3px 0}
41
+ .row label{flex:1;color:#aaa;display:flex;align-items:center;gap:4px}
42
+ .row input[type=number]{width:90px}
43
+
44
+ .presets{display:flex;flex-wrap:wrap;gap:4px;margin:6px 0}
45
+ .presets button{font-size:11px;padding:2px 6px}
46
+
47
+ /* ---------- add row + custom autocomplete dropdown ---------- */
48
+ #addrow{display:flex;gap:4px}
49
+ .ac-wrap{position:relative;flex:1}
50
+ .ac-wrap input{width:100%}
51
+ .ac{position:absolute;left:0;right:0;top:100%;margin-top:2px;z-index:20;max-height:260px;overflow:auto;
52
+ background:#15171b;border:1px solid #3a4250;border-radius:4px;box-shadow:0 6px 18px #000a}
53
+ .ac[hidden]{display:none}
54
+ .ac-opt{display:flex;align-items:baseline;gap:8px;padding:3px 8px;cursor:pointer;white-space:nowrap}
55
+ .ac-opt:hover,.ac-opt.sel{background:#26313f}
56
+ .ac-spec{color:#cde}
57
+ .ac-hint{margin-left:auto;color:#778;font-size:10px}
58
+
59
+ /* ---------- live signal list (merged signals + stats) ---------- */
60
+ #siglist{display:flex;flex-direction:column;gap:3px;margin-top:6px}
61
+ .sig{border:1px solid #232323;border-radius:4px;padding:4px 5px;background:#141414}
62
+ .sig.off{opacity:.45}
63
+ .sighead{display:flex;align-items:center;gap:6px}
64
+ .sighead .signame{flex:1;cursor:pointer;word-break:break-all;color:#dde}
65
+ .sighead .enc{color:#777;font-size:10px}
66
+ .sw{width:16px;height:16px;padding:0;border:1px solid #3a3a3a;border-radius:3px;cursor:pointer;background:none;vertical-align:middle;flex:none}
67
+ .sw::-webkit-color-swatch-wrapper{padding:0}
68
+ .sw::-webkit-color-swatch{border:none;border-radius:2px}
69
+ .sw::-moz-color-swatch{border:none;border-radius:2px}
70
+ .rm{color:#a44;cursor:pointer;font-weight:bold;padding:0 4px;flex:none}
71
+ .rm:hover{color:#f66}
72
+ .stats{font-size:10px;color:#999;margin-top:2px;padding-left:22px;display:flex;flex-wrap:wrap;gap:0 8px}
73
+ .stats b{color:#ccc;font-weight:normal}
74
+
75
+ #noSigs{padding:8px 4px;color:#777;line-height:1.7;font-size:11px}
76
+ #noSigs b{color:#9c9;font-weight:normal}
77
+ #noSigs code{background:#1c1c1c;border:1px solid #2c2c2c;border-radius:3px;padding:0 4px;color:#cda}
78
+
79
+ #toast{position:fixed;top:8px;right:8px;max-width:380px;display:flex;flex-direction:column;gap:4px;z-index:100}
80
+ .t{background:#3a1a1a;border:1px solid #a44;color:#fcc;padding:6px 10px;border-radius:4px;font-size:11px;box-shadow:0 2px 8px #000a}
81
+ .t.info{background:#1a2a3a;border-color:#48a;color:#cdf}
82
+ .muted{color:#777}
83
+
84
+ /* connection indicator (top bar) */
85
+ #bar .conn{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:#999}
86
+ .cdot{font-size:11px;line-height:1;transition:color .2s}
87
+ .cdot.ok{color:#5c5}
88
+ .cdot.idle{color:#cb5}
89
+ .cdot.retry{color:#c95;animation:blink 1s steps(1) infinite}
90
+ .cdot.down{color:#a44}
91
+ @keyframes blink{50%{opacity:.25}}
92
+ #reconnect{font-size:11px;padding:2px 6px}
93
+
94
+ /* idle / ended banner — unobtrusive strip under the top bar */
95
+ .banner{padding:4px 10px;font-size:11px;border-bottom:1px solid #333;background:#1a1d22;color:#bb9}
96
+ .banner.ended{background:#221a1a;color:#caa}
97
+ .banner.live{display:none!important}