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.
- package/dist/config.d.ts +7 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +125 -25
- package/dist/index.js.map +1 -1
- package/dist/tools/stm-build.d.ts +23 -0
- package/dist/tools/stm-build.js +78 -0
- package/dist/tools/stm-build.js.map +1 -0
- package/dist/tools/stm-flash.d.ts +36 -0
- package/dist/tools/stm-flash.js +112 -0
- package/dist/tools/stm-flash.js.map +1 -0
- package/dist/tools/trace-session.d.ts +12 -0
- package/dist/tools/trace-session.js +69 -0
- package/dist/tools/trace-session.js.map +1 -1
- package/dist/tools/trace-webui.d.ts +14 -7
- package/dist/tools/trace-webui.js +77 -33
- package/dist/tools/trace-webui.js.map +1 -1
- package/dist/tools/trace-write.d.ts +9 -0
- package/dist/tools/trace-write.js +32 -0
- package/dist/tools/trace-write.js.map +1 -0
- package/dist/utils/userConfig.d.ts +9 -0
- package/dist/utils/userConfig.js.map +1 -1
- package/package.json +2 -1
- package/skills/swd-tracer/SKILL.md +48 -0
- package/tracer/PROTOCOL.md +69 -0
- package/tracer/swd_tracer.py +449 -12
- package/tracer/ui/config.js +40 -0
- package/tracer/ui/connection.js +170 -0
- package/tracer/ui/index.html +50 -798
- package/tracer/ui/main.js +30 -0
- package/tracer/ui/plot.js +337 -0
- package/tracer/ui/signals.js +39 -0
- package/tracer/ui/state.js +57 -0
- package/tracer/ui/stats.js +52 -0
- package/tracer/ui/style.css +97 -0
- package/tracer/ui/symbols.js +80 -0
- package/tracer/ui/toolbar.js +95 -0
- package/tracer/ui/util.js +62 -0
- package/tracer/ui/watchlist.js +179 -0
- package/vscode-extension/README.md +32 -0
- package/vscode-extension/extension.js +56 -0
- 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}
|