Files
DCU_DesktopController/js/motor/scope.js
T
cbb aa1e14d4cb refactor(motor): 示波器外提 scope.js(P1 重做第 1 步)
从完好 main.js 机械外提示波器 IIFE 为 createScopeSys;
STATUS 契约 offlineOverlay/statusLive 保持不动。
UI 验收:overlay display none、电机数据在、无控制台报错。
2026-09-21 03:08:20 +08:00

278 lines
10 KiB
JavaScript

/**
* scope.js — 电机控制台示波器(三相大图 + 迷你卡片波形)
* 从 main.js 示波器 IIFE 机械外提;行为不变。
* createScopeSys(ctx) 注入 state / motorKey / clampNum / $。
* 返回 { reset }:切电机/切通道时清历史。
*/
"use strict";
/** @param {{state:any, motorKey:(m:any)=>string, clampNum:(v:number,lo:number,hi:number)=>number, $:(s:string)=>Element|null}} ctx */
export function createScopeSys(ctx) {
const { state, motorKey, clampNum, $ } = ctx;
const BUF = 300;
const dprOf = () => window.devicePixelRatio || 1;
const hist = {
u: new Float32Array(BUF), v: new Float32Array(BUF), w: new Float32Array(BUF),
angle: new Float32Array(BUF), velocity: new Float32Array(BUF), current: new Float32Array(BUF),
mcuTemp: new Float32Array(BUF), motTemp: new Float32Array(BUF),
};
let head = 0;
const valid = new Uint8Array(BUF);
function colors() {
const cs = getComputedStyle(document.documentElement);
return {
u: cs.getPropertyValue("--seed-primary").trim() || "#22d3ee",
v: "#34d399",
w: cs.getPropertyValue("--seed-accent").trim() || "#f59e0b",
};
}
let C = colors();
setInterval(() => { C = colors(); }, 2000);
const sizes = new WeakMap();
let sizeObs = null;
function cssSize(canvas) {
let s = sizes.get(canvas);
if (s) return s;
const r = canvas.getBoundingClientRect();
s = { w: r.width, h: r.height };
sizes.set(canvas, s);
if (!sizeObs) {
sizeObs = new ResizeObserver((entries) => {
for (const e of entries) {
const t = sizes.get(e.target);
if (!t) continue;
const b = e.borderBoxSize && e.borderBoxSize[0];
t.w = b ? b.inlineSize : e.contentRect.width;
t.h = b ? b.blockSize : e.contentRect.height;
}
});
}
sizeObs.observe(canvas);
return s;
}
function fit(canvas, c2d) {
const dpr = dprOf();
const s = cssSize(canvas);
if (s.w === 0) return null;
const cw = Math.round(s.w * dpr), ch = Math.round(s.h * dpr);
if (canvas.width !== cw || canvas.height !== ch) {
canvas.width = cw;
canvas.height = ch;
}
c2d.setTransform(dpr, 0, 0, dpr, 0, 0);
return s;
}
function grid(c2d, w, h) {
c2d.strokeStyle = "rgba(255,255,255,0.05)";
c2d.lineWidth = 1;
for (let i = 1; i < 4; i++) { const y = h / 4 * i; c2d.beginPath(); c2d.moveTo(0, y); c2d.lineTo(w, y); c2d.stroke(); }
for (let i = 1; i < 6; i++) { const x = w / 6 * i; c2d.beginPath(); c2d.moveTo(x, 0); c2d.lineTo(x, h); c2d.stroke(); }
c2d.strokeStyle = "rgba(255,255,255,0.13)";
c2d.beginPath();
c2d.moveTo(0, h / 2); c2d.lineTo(w, h / 2); c2d.stroke();
}
const scratchY = new Float32Array(BUF);
const okAt = (i) => valid[(head + i) % BUF] === 1;
function drawTrace(c2d, buf, color, yOf, base, w, lw, fill) {
const xOf = (i) => i / (BUF - 1) * w;
for (let i = 0; i < BUF; i++) scratchY[i] = yOf(buf[(head + i) % BUF]);
const linePath = (a, b) => {
c2d.beginPath();
c2d.moveTo(xOf(a), scratchY[a]);
for (let i = a + 1; i <= b; i++) c2d.lineTo(xOf(i), scratchY[i]);
};
c2d.lineJoin = "round"; c2d.strokeStyle = color;
let start = 0;
while (start < BUF - 1) {
const solid = okAt(start) && okAt(start + 1);
let end = start + 1;
while (end + 1 < BUF && (okAt(end) && okAt(end + 1)) === solid) end++;
if (solid && fill) {
linePath(start, end);
c2d.lineTo(xOf(end), base); c2d.lineTo(xOf(start), base); c2d.closePath();
c2d.fillStyle = color + "16";
c2d.fill();
}
linePath(start, end);
c2d.setLineDash(solid ? [] : [5, 4]);
if (solid) {
c2d.globalAlpha = 0.16; c2d.lineWidth = lw * 3.2; c2d.stroke();
c2d.globalAlpha = 1;
}
c2d.lineWidth = lw; c2d.stroke();
start = end;
}
}
function trace(c2d, buf, color, range, w, h, lw, fill) {
const mid = h / 2, gain = (h / 2 - 4) / range;
drawTrace(c2d, buf, color, (v) => mid - Math.max(-range, Math.min(range, v)) * gain, mid, w, lw, fill);
}
function traceUni(c2d, buf, color, min, max, w, h, lw, fill) {
const span = max - min;
drawTrace(c2d, buf, color, (v) => h - 4 - Math.max(0, Math.min(1, (v - min) / span)) * (h - 8), h, w, lw, fill);
}
const big = $("#scopeCanvas");
const bigCtx = big.getContext("2d");
function drawBig() {
const s = fit(big, bigCtx);
if (!s) return;
const w = s.w, h = s.h;
bigCtx.clearRect(0, 0, w, h);
grid(bigCtx, w, h);
trace(bigCtx, hist.u, C.u, 5000, w, h, 1.6, false);
trace(bigCtx, hist.v, C.v, 5000, w, h, 1.6, false);
trace(bigCtx, hist.w, C.w, 5000, w, h, 1.6, false);
}
const MINIS = {
angle: { el: $("#miniAngle"), buf: hist.angle, auto: true, unit: "°", color: () => C.u },
velocity: { el: $("#miniVelocity"), buf: hist.velocity, auto: true, unit: "deg/s", color: () => C.v },
current: { el: $("#miniCurrent"), buf: hist.current, auto: true, unit: "mA", color: () => C.w },
mcuTemp: { el: $("#miniMcuTemp"), buf: hist.mcuTemp, uni: { min: 20, max: 100 }, ticks: [100, 60, 20], unit: "°C", color: () => C.v },
motTemp: { el: $("#miniMotTemp"), buf: hist.motTemp, uni: { min: 20, max: 100 }, ticks: [100, 60, 20], unit: "°C", color: () => C.w },
};
const MINI_LIST = Object.entries(MINIS).map(([sig, m]) => {
m.ctx = m.el.getContext("2d");
m.card = document.querySelector(`[data-signal="${sig}"]`);
return m;
});
const MONO = '"Cascadia Code","Fira Code",Consolas,monospace';
const fmtTick = (v) => (Math.abs(v) >= 1000 ? (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1) + "k" : String(v));
function autoScale(buf) {
let lo = Infinity, hi = -Infinity, hasNonZero = false;
for (let i = 0; i < buf.length; i++) {
const v = buf[i];
if (!Number.isFinite(v)) continue;
if (v < lo) lo = v;
if (v > hi) hi = v;
if (v !== 0) hasNonZero = true;
}
if (!hasNonZero || !Number.isFinite(lo) || !Number.isFinite(hi)) {
return { min: -1, max: 1, ticks: [1, 0, -1] };
}
if (lo === hi) {
const c = lo;
const pad = Math.abs(c) * 0.2 || 1;
return { min: c - pad, max: c + pad, ticks: [c + pad, c, c - pad] };
}
const span = hi - lo;
const pad = span * 0.1 || 1;
let min = lo - pad, max = hi + pad;
const niceStep = niceStepSize(max - min, 3);
min = Math.floor(min / niceStep) * niceStep;
max = Math.ceil(max / niceStep) * niceStep;
const ticks = [max, (max + min) / 2, min];
return { min, max, ticks };
}
function niceStepSize(range, targetTicks) {
const rawStep = range / targetTicks;
const mag = Math.pow(10, Math.floor(Math.log10(rawStep)));
const norm = rawStep / mag;
let step;
if (norm < 1.5) step = 1;
else if (norm < 3) step = 2;
else if (norm < 7) step = 5;
else step = 10;
return step * mag;
}
function drawYScale(c2d, m, w, h) {
const yOf = m.uni
? (v) => h - 4 - Math.max(0, Math.min(1, (v - m.uni.min) / (m.uni.max - m.uni.min))) * (h - 8)
: (v) => h / 2 - Math.max(-m.range, Math.min(m.range, v)) * ((h / 2 - 4) / m.range);
c2d.save();
c2d.font = "8px " + MONO;
c2d.textBaseline = "middle";
for (const t of m.ticks) {
const y = yOf(t);
c2d.strokeStyle = "rgba(226,233,242,0.35)";
c2d.lineWidth = 1;
c2d.beginPath(); c2d.moveTo(0, y); c2d.lineTo(5, y); c2d.stroke();
c2d.fillStyle = "rgba(226,233,242,0.5)";
c2d.fillText(fmtTick(Math.round(t)), 7, y);
}
if (m.unit) {
c2d.textBaseline = "top";
c2d.fillStyle = "rgba(226,233,242,0.35)";
c2d.fillText(m.unit, w - c2d.measureText(m.unit).width - 4, 3);
}
c2d.restore();
}
function drawMini(m) {
const s = fit(m.el, m.ctx);
if (!s) return;
const w = s.w, h = s.h;
if (m.auto) {
const sc = autoScale(m.buf);
m.uni = { min: sc.min, max: sc.max };
m.ticks = sc.ticks;
}
m.ctx.clearRect(0, 0, w, h);
grid(m.ctx, w, h);
if (m.uni) traceUni(m.ctx, m.buf, m.color(), m.uni.min, m.uni.max, w, h, 1.8, true);
else trace(m.ctx, m.buf, m.color(), m.range, w, h, 1.8, true);
drawYScale(m.ctx, m, w, h);
}
document.querySelectorAll(".big-reading, .temp-block").forEach((card) => {
card.addEventListener("click", () => card.classList.toggle("open"));
});
function sample() {
const n = state.selectedNode;
const motor = n != null ? state.motors.find((m) => m.node === n && m.ch === state.ch) : null;
const d = motor ? state.decoded.get(motorKey(motor)) : null;
const online = d && motor && motor.online;
valid[head] = online ? 1 : 0;
if (!online) {
hist.u[head] = hist.v[head] = hist.w[head] = 0;
hist.angle[head] = hist.velocity[head] = hist.current[head] = 0;
hist.mcuTemp[head] = hist.motTemp[head] = 0;
} else {
const targets = { u: d.uCurrent, v: d.vCurrent, w: d.wCurrent };
for (const ch of ["u", "v", "w"]) {
const prev = hist[ch][(head + BUF - 1) % BUF];
hist[ch][head] = prev + (clampNum(targets[ch], -5000, 5000) - prev) * 0.5;
}
const pA = hist.angle[(head + BUF - 1) % BUF];
hist.angle[head] = pA + (d.rtAngle - pA) * 0.4;
const pV = hist.velocity[(head + BUF - 1) % BUF];
hist.velocity[head] = pV + (d.rtVelocity - pV) * 0.4;
const pC = hist.current[(head + BUF - 1) % BUF];
hist.current[head] = pC + (d.rtCurrent - pC) * 0.4;
const pMcu = hist.mcuTemp[(head + BUF - 1) % BUF];
hist.mcuTemp[head] = pMcu + (d.mcuTemp - pMcu) * 0.1;
const pMot = hist.motTemp[(head + BUF - 1) % BUF];
hist.motTemp[head] = pMot + (d.motTemp - pMot) * 0.1;
}
head = (head + 1) % BUF;
}
let lastSample = 0;
let dirty = true;
function loop(ts) {
if (ts - lastSample > 33) { sample(); lastSample = ts; dirty = true; }
if (dirty) {
dirty = false;
drawBig();
for (const m of MINI_LIST) {
if (m.card && m.card.classList.contains("open")) drawMini(m);
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
function reset() {
for (const key of Object.keys(hist)) hist[key].fill(0);
valid.fill(0);
}
return { reset };
}