<meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>变桨液压回路仿真台 · SWT-4.0-130</title> <meta name="google" content="notranslate"> <style> :root{ --bg:#F4F6F8; --panel:#FFFFFF; --panel2:#EDF0F3; --ink:#10151B; --sub:#5A6472; --line:#D8DDE3; --grid:#E4E8EC; --hp:#C8302B; --lp:#1E7FC2; --gas:#D99A2B; --ok:#2E8B57; --warn:#B7791F; --chipbg:#EDF0F3; --focus:#1E7FC2; } @media (prefers-color-scheme: dark){:root{ --bg:#0D1117; --panel:#151B23; --panel2:#1C242E; --ink:#E8EBEF; --sub:#93A0AD; --line:#2A3340; --grid:#222B36; --hp:#E05A50; --lp:#4FA3DC; --gas:#E2AC4A; --ok:#4CAF7D; --warn:#D9A24A; --chipbg:#1C242E; }} :root[data-theme="dark"]{ --bg:#0D1117; --panel:#151B23; --panel2:#1C242E; --ink:#E8EBEF; --sub:#93A0AD; --line:#2A3340; --grid:#222B36; --hp:#E05A50; --lp:#4FA3DC; --gas:#E2AC4A; --ok:#4CAF7D; --warn:#D9A24A; --chipbg:#1C242E; } :root[data-theme="light"]{ --bg:#F4F6F8; --panel:#FFFFFF; --panel2:#EDF0F3; --ink:#10151B; --sub:#5A6472; --line:#D8DDE3; --grid:#E4E8EC; --hp:#C8302B; --lp:#1E7FC2; --gas:#D99A2B; --ok:#2E8B57; --warn:#B7791F; --chipbg:#EDF0F3; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink); font:14px/1.55 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;} .mono{font-family:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace; font-variant-numeric:tabular-nums;} header{padding:14px 18px 10px;border-bottom:1px solid var(--line)} header h1{margin:0;font-size:17px;font-weight:700;letter-spacing:.02em} header .sub{color:var(--sub);font-size:12px;margin-top:2px} .bar{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;padding:10px 18px; border-bottom:1px solid var(--line);background:var(--panel)} .bar .grp{display:flex;gap:6px;align-items:center;flex-wrap:wrap} .bar .lab{font-size:11px;color:var(--sub);letter-spacing:.08em;text-transform:uppercase} button{font:inherit;color:var(--ink);background:var(--chipbg);border:1px solid var(--line); border-radius:5px;padding:5px 12px;cursor:pointer} button:hover{border-color:var(--sub)} button:focus-visible{outline:2px solid var(--focus);outline-offset:1px} button.on{background:var(--ink);color:var(--bg);border-color:var(--ink)} button.estop{border-color:var(--hp);color:var(--hp);font-weight:700} button.estop.on{background:var(--hp);color:#fff} main{display:grid;grid-template-columns:305px minmax(430px,1fr) 355px;gap:12px; padding:12px 18px;align-items:start} .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px} .card h3{margin:0 0 8px;font-size:12px;letter-spacing:.08em;color:var(--sub); text-transform:uppercase;font-weight:600} .fault{margin-bottom:14px} .fault .name{display:flex;justify-content:space-between;align-items:baseline;font-weight:600;font-size:13px} .fault .val{color:var(--hp)} .fault input[type=range]{width:100%;accent-color:var(--hp)} .fault .hint{font-size:11.5px;color:var(--sub);margin-top:1px} .presets{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px} .presets button{font-size:12px;padding:3px 9px} table.mx{width:100%;border-collapse:collapse;font-size:12.5px} table.mx th,table.mx td{padding:3.5px 4px;text-align:right;border-bottom:1px solid var(--grid)} table.mx th:first-child,table.mx td:first-child{text-align:left} table.mx thead th{color:var(--sub);font-weight:600;font-size:11px} canvas{display:block;width:100%;border:1px solid var(--line);border-radius:8px;background:var(--panel)} .split{display:grid;grid-template-columns:290px 1fr;gap:12px;margin-top:12px} .verdict{margin-top:10px;padding:9px 12px;border-radius:6px;font-size:13px;display:none} .verdict.ok{display:block;background:rgba(46,139,87,.12);border:1px solid var(--ok)} .verdict.bad{display:block;background:rgba(200,48,43,.12);border:1px solid var(--hp)} .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} .chip{font-size:11.5px;background:var(--chipbg);border:1px solid var(--line); border-radius:4px;padding:3px 8px;color:var(--sub)} footer{padding:6px 18px 26px;color:var(--sub);font-size:12px;max-width:1100px} footer b{color:var(--ink)} label.tg{display:inline-flex;gap:5px;align-items:center;cursor:pointer;font-size:12.5px} input[type=checkbox]{accent-color:var(--hp)} @media (max-width:1240px){main{grid-template-columns:290px 1fr}.right{grid-column:1/-1; display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px}} @media (max-width:860px){main{grid-template-columns:1fr}.split{grid-template-columns:1fr}} @media (prefers-reduced-motion: reduce){.anim{display:none}} </style><style id="guanlan-pitch-light"> :root,:root[data-theme="dark"],:root[data-theme="light"]{color-scheme:light;--bg:#f2f7fa;--panel:#fff;--panel2:#eaf3f9;--ink:#19374b;--sub:#526d80;--line:#ccdee9;--grid:#e1ebf2;--chipbg:#edf5fa;--focus:#216b8a;--hp:#C8302B;--lp:#1E7FC2;--gas:#D99A2B;--ok:#2E8B57;--warn:#B7791F} body{background:var(--bg);color:var(--ink)}header{padding:24px 26px;background:linear-gradient(115deg,#e4f1f9,#f3f9fc);border-bottom:1px solid #c8deeb}header h1{font-size:22px;line-height:1.5;color:#16455f}header .sub{max-width:1150px;line-height:1.8;margin-top:8px}.bar{padding:14px 24px;gap:12px 20px;background:#fff}button{border-radius:8px;padding:7px 12px}button.on:not(.estop){background:#216b8a;border-color:#216b8a;color:#fff}main{padding:20px 24px;gap:18px}.card{border-radius:12px;padding:18px;box-shadow:0 3px 14px #214e6c08}.card h3{color:#216b8a;background:#edf5fa;border-left:3px solid #469ab8;padding:9px 11px;border-radius:4px;letter-spacing:.03em;font-size:13px}.fault{margin-bottom:18px}.fault .hint{line-height:1.7}canvas{border-radius:10px}table.mx th,table.mx td{padding:7px 5px}table.mx thead{background:#edf5fa}.chip{border-radius:6px;padding:5px 8px}footer{padding:12px 24px 28px}@media(max-width:600px){main{padding:12px;gap:12px}header{padding:18px}header h1{font-size:18px}.bar{padding:12px}.right{grid-template-columns:minmax(0,1fr)}} </style> <style id="pitch-visual-banner">header{position:relative;isolation:isolate;overflow:hidden;background:linear-gradient(110deg,#0c2e40,#124c5d);border-color:#326274;padding:26px}header:after{content:"";position:absolute;z-index:-1;right:0;top:0;width:40%;height:100%;opacity:.32;background-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20520%20320%22%3E%3Cdefs%3E%3Cpattern%20id%3D%22g%22%20width%3D%2226%22%20height%3D%2226%22%20patternUnits%3D%22userSpaceOnUse%22%3E%3Cpath%20d%3D%22M26%200H0V26%22%20fill%3D%22none%22%20stroke%3D%22%2372b8c3%22%20stroke-opacity%3D%22.1%22%2F%3E%3C%2Fpattern%3E%3C%2Fdefs%3E%3Cpath%20fill%3D%22url%28%23g%29%22%20d%3D%22M0%200h520v320H0z%22%2F%3E%3Cg%20fill%3D%22none%22%20stroke%3D%22%2367bbb9%22%20stroke-width%3D%221.7%22%20opacity%3D%22.65%22%3E%3Cpath%20d%3D%22M22%20262Q142%20224%20272%20257T505%20245M22%20276Q142%20238%20272%20271T505%20259M22%20290Q142%20252%20272%20285T505%20273%22%2F%3E%3Cpath%20d%3D%22M152%20253V131m0%200%204-81%206%202-5%2074%2061%2039-4%205-66-36-58%2046-4-5%2060-49M291%20246V98m0%200%204-79%206%202-5%2072%2061%2039-4%205-66-36-58%2046-4-5%2060-49M405%20264V158m0%200%203-62%205%202-4%2057%2047%2030-3%204-52-29-45%2036-3-4%2048-36%22%2F%3E%3Ccircle%20cx%3D%22152%22%20cy%3D%22131%22%20r%3D%224%22%20fill%3D%22%2367bbb9%22%2F%3E%3Ccircle%20cx%3D%22291%22%20cy%3D%2298%22%20r%3D%224%22%20fill%3D%22%2367bbb9%22%2F%3E%3Ccircle%20cx%3D%22405%22%20cy%3D%22158%22%20r%3D%224%22%20fill%3D%22%2367bbb9%22%2F%3E%3Cpath%20d%3D%22M35%20248v-53h63v50m-68-51%208-9h60l7%209zm16%202v48m17-48v45m17-45v46M40%20172h39m-21-12h41%22%2F%3E%3Cpath%20stroke-dasharray%3D%224%206%22%20d%3D%22M78%20250Q240%20190%20452%20238%22%2F%3E%3C%2Fg%3E%3Ccircle%20cx%3D%22465%22%20cy%3D%2228%22%20r%3D%2273%22%20fill%3D%22none%22%20stroke%3D%22%238fcbb5%22%20stroke-opacity%3D%22.12%22%2F%3E%3Ccircle%20cx%3D%22465%22%20cy%3D%2228%22%20r%3D%2299%22%20fill%3D%22none%22%20stroke%3D%22%238fcbb5%22%20stroke-opacity%3D%22.06%22%20stroke-width%3D%2220%22%2F%3E%3C%2Fsvg%3E");background-size:contain;background-repeat:no-repeat;background-position:right center}header h1{color:#fff}header h1 span{color:#bfdce6!important}header .sub{color:#bfdce6;font-size:12px;line-height:1.8}.card{border-top:3px solid #75aebd}.card h3{background:#eaf3f7;border-left-color:#1f8296}@media(max-width:600px){header{padding:18px}header:after{width:100%;opacity:.13}}</style><header> <h1>变桨液压回路仿真台 <span class="mono" style="font-weight:400;color:var(--sub)">SWT-4.0-130 · 图纸 1034384/1034386</span></h1> <div class="sub">泵持续运转 + 25 号阀滞环卸荷 + <b>双蓄能器</b>(泵站 70 号 10L/180bar 近端缓冲 · 轮毂 106 6×30L/125bar 顺桨主源)—— 参数标定自本场 38 台实测与原厂图纸;分别注入失效,看两者的签名有何不同</div> </header> <div class="bar"> <div class="grp"><span class="lab">工况</span> <button id="m-reg" class="on">运行调节</button> <button id="m-close">收桨 0→82°</button> <button id="m-open">开桨 82→0°</button> <button id="m-estop" class="estop">紧急顺桨</button> </div> <div class="grp"><span class="lab">电源</span> <button id="pw" class="on">有电</button> </div> <div class="grp"><span class="lab">倍速</span> <input id="spd" type="range" min="1" max="40" value="10" style="width:110px"> <span id="spdv" class="mono" style="font-size:12px">10×</span> </div> <div class="grp"><span class="lab">预设</span> <div class="presets"> <button data-p="healthy">健康台</button> <button data-p="early">70号早期失氮</button> <button data-p="collapse">70号失效=复现 8#</button> <button data-p="hubfail">106失氮(假设)</button> <button data-p="bothfail">双失效(最不利)</button> <button data-p="leak">复现 25# 泄漏</button> <button data-p="pumpint">复现 14# 泵间歇</button> <button data-p="stuck">70号卡滞(假设)</button> </div> </div> </div> <main> <div> <div class="card"> <h3>失效注入</h3> <div class="fault"> <div class="name"><span>① 泵站70号失氮 <span style="color:var(--sub);font-weight:400">10L/180bar</span></span> <span class="val mono" id="v-p070">180 bar</span></div> <input type="range" id="s-p070" min="0" max="180" value="180" step="1"> <div class="hint">180=铭牌 · ≈0–2 匹配 8# 实测(带宽135/周期2.6s) · A 类失效部位(现场证实, 4# 换件先例)</div> <label class="tg" style="margin-top:4px"><input type="checkbox" id="s-stuck"> 70号活塞卡滞 <span style="color:var(--sub)">— 候选机理(未证实),油气零交换</span></label> </div> <div class="fault"> <div class="name"><span>② 轮毂106失氮 <span style="color:var(--sub);font-weight:400">6×30L/125bar</span></span> <span class="val mono" id="v-p0H">125 bar</span></div> <input type="range" id="s-p0H" min="0" max="160" value="125" step="1"> <div class="hint">125=铭牌@20℃ · 2026-08 现场气侧实测≈140(夏季温修内,正常) · 拖低=假设情形: 循环仅中度变化、失电顺桨失败</div> </div> <div class="fault"> <div class="name"><span>③ 系统泄漏</span><span class="val mono" id="v-leak">1.0×</span></div> <input type="range" id="s-leak" min="10" max="60" value="10" step="1"> <div class="hint">泄漏基线 2.2 L/min(+调节变桨≈1.0=实测总消耗3.2)· 25# ≈ 2.8×</div> </div> <div class="fault"> <div class="name"><span>④ 泵性能</span><span class="val mono" id="v-pump">100%</span></div> <input type="range" id="s-pump" min="20" max="100" value="100" step="1"> <div class="hint">磨损/容积效率下降 → 加载时间变长</div> <div class="name" style="margin-top:6px"><span> 间歇失供</span><span class="val mono" id="v-int">关</span></div> <input type="range" id="s-int" min="0" max="100" value="0" step="1"> <div class="hint">随机掉供数十秒—数分钟 → 复现 14#/32# 的分钟级低压塌陷</div> </div> <div class="verdict" id="verdict"></div> </div> <div class="card" style="margin-top:12px"> <h3>SCADA 派生指标(滚动 10 min)</h3> <table class="mx mono"> <thead><tr><th>指标</th><th>仿真</th><th>健康实测</th><th>8# 实测</th></tr></thead> <tbody id="mx"></tbody> </table> </div> </div> <div> <div id="circuit-mode-caption" style="padding:14px 18px;background:#eaf4f7;color:#163546;line-height:1.8;border:1px solid #cfe1e8;border-radius:10px 10px 0 0;overflow-wrap:anywhere"></div><canvas id="cv" height="520" data-aspect="0.5"></canvas> <div class="split"> <canvas id="pv" height="330"></canvas> <div class="card"> <h3>活塞与进油量 · 逐循环</h3> <table class="mx mono"><tbody id="pistab"></tbody></table> <div class="chips"> <span class="chip mono">ΔV = C · Δp</span> <span class="chip mono">C = 管路 + C70 + 0.095·C106</span> <span class="chip mono">Cᵢ = Vᵢ·p₀ᵢ / p²</span> <span class="chip mono">周期 ≈ C·带宽 ÷ 消耗</span> </div> </div> </div> </div> <div class="right"> <canvas id="c1" height="200"></canvas> <canvas id="c2" height="170"></canvas> <canvas id="c3" height="170"></canvas> </div> </main> <footer> <b>边界与简化(读图前请先知道):</b> ① 单节点压力模型 + 「棘轮」叠加显示;双蓄能器按失效部位改判(v47p,2026-08-13 现场证实)的定量闭合参数化: 泵站 70 号(10L/180bar,ø7.0 节流+20×2.5 管)直连泵压主管<b>全额参与</b>充能循环, 轮毂 106(6×30L/125bar)经长管+旋转接头+118 单向阀<b>弱耦合</b>(kH=0.095 标定,健康总 C 对齐实测 0.087 L/bar); 70 号垮塌 → 循环尖峰 → 棘轮抬升 → 118 恒闭 → 轮毂被踢出循环(C 塌到纯管路 ≈0.003,对齐 8# 实测 0.0037)——此连锁按参数化关系建模,非流网求解; ② <b>三种失效的签名差异(本台的教学核心)</b>:70 号失效 = SCADA 循环签名剧变(吞吐 0.5L/周期 2.6s/带宽 135)而失电顺桨不受影响(数据窗 4 次全场停电全部安全顺桨、零超速 = 实证); 106 失氮 = 循环仅中度变化(吞吐 ~2L,与个体基线难分辨)而失电顺桨失去能量源 —— SCADA 判据对它几乎盲,覆盖它的是机组自检容量检查(UP03284 组,从轮毂 235bar 起测); 双失效 = SCADA 上与单独 70 号失效不可分辨 + 顺桨失败。<b>两套监测互补,缺一即盲</b>; ③ 紧急顺桨用轮毂 106 设计容量 6×30L 与等温气体定律,缸驱动背压取 25 bar(气动力矩未建模)——「低预充仍勉强完成」是理想下界,实机以现场实测为准;70 号对顺桨的贡献(&lt;2L,约 7%)未建模; ④ 预设「复现 X# 签名」意为让派生指标匹配该台实测,其 p₀ 数值不是实测氮压;「106失氮」是<b>假设情形</b>——本场 106 已实测正常(2026-08 气侧 ≈140 bar)。 数据源:图纸 1034384/1034386 · 实机参数 UP07200/07228(245/195)· 38 台 SCADA 实测(2025-01→2026-07)· 2026-08-13 现场核实(70 号换件履历 + 106 气侧实测)。 </footer> <script> 'use strict'; /* ================= 物理内核(标定值,来源见页脚) ================= */ const K = { Qp: 36.8/60, // 变桨泵 L/s(25.4ccm×1450rpm) pLoad: 194, pUnload: 246, pRelief: 280, dLoad: 0.80, dUnload: 0.14, // 加载/卸荷响应延迟 s(五台反推 0.80±0.03) V70: 10, p70n: 180, // 泵站 70 号手刹蓄能器 10 L / 180 bar(ø7.0 节流+20×2.5 管直连泵压主管) V0H: 180, p0Hn: 125, // 轮毂 106 蓄能器 6×30 L / 125 bar(旋转接头+118 之后,顺桨主源) kH: 0.095, // 轮毂对充能循环的弱耦合系数(标定: 健康总 C 对齐实测 0.087 L/bar) Cline: 0.0025, // 管路/油柔度残余容变 L/bar(对齐 8# 塌陷签名 0.50L/2.6s/带宽135) LpD: 0.2642, LpDb: 0.0881, // 总/单叶片 L/度(环形腔 ø125/90×611) qLeak0: 2.2/60, // 泄漏基线 L/s @220bar (总消耗3.2 = 此项 + 调节变桨≈1.0) driveP: 25, // 缸驱动背压假设 bar d121: 2.4, // 顺桨节流孔 mm(每叶片块一只) rho: 870, Cd: 0.62, T0: 38, TperKW: 3.2, Ttau: 240, // 油温:+4 K/kW,τ=240 s }; const orifQ = (mm, dp) => dp<=0 ? 0 : K.Cd*Math.PI/4*(mm/1000)**2*Math.sqrt(2*dp*1e5/K.rho)*1000; // L/s /* ================= 状态 ================= */ const S = { t:0, p:220, pH:238, th:2, mode:'reg', power:true, v25:false, v25cmd:false, delayT:0, // 阀 25(true=得电=加载) Vgas:0, // 顺桨期气腔体积(动态) estop:null, // {t0, done, stall...} oilT:38, thCmd:2, regPh:0, intState:0, intT:0, // 间歇失供状态机 cyc:[], lastUnload:0, loadStart:null, lastVc:null, lastPer:null, buf:[], acc:0, ratchet:238, }; const F = { p070:180, p0H:125, stuck:false, leak:1.0, pump:1.0, inter:0 }; /* 单罐等温容变 L/bar(V=总容积, p0=预充;p≤p0 活塞顶死, 只剩止点软区) */ function accC(V, p0, p){ if (p0 < 0.5) return 0; if (p <= p0) return 0.02; return V*p0/(p*p); } /* 70号垮塌 → 尖峰 → 棘轮抬升 → 118 恒闭 → 轮毂被踢出循环(v47p 连锁, 参数化) */ function hubGate(){ return F.stuck ? 0 : Math.max(0, Math.min(1, (F.p070-20)/60)); } /* 充能循环等效容变 = 管路 + 泵站70号(全额) + 轮毂106(弱耦合×连锁门) */ function Ccharge(p){ let c = K.Cline; if (!F.stuck) c += accC(K.V70, F.p070, p); c += K.kH * hubGate() * accC(K.V0H, F.p0H, p); return c; } /* 稳态气腔占比(显示用) */ function gasFrac(p, p0){ if (p0 < 0.5) return 0; if (p <= p0) return 1; return Math.min(1, p0/p); } /* ================= 仿真步 ================= */ function step(dt){ S.t += dt; const perEst = Math.max(1.6, S.lastPer ?? 90); const qCycle = 0.45/perEst; // 阀切换/瞬态每循环开销 ≈0.45 L (健康台 0.3 L/min, 塌陷台 ~10 L/min) const leak = K.qLeak0*F.leak*(S.p/220) + qCycle; /* 泵可用性 */ let pumpOn = S.power && S.mode!=='estop'; if (pumpOn && F.inter>0){ // 间歇失供 S.intT -= dt; if (S.intState===0 && S.intT<=0){ S.intState=1; S.intT=15+F.inter*165*Math.random(); } else if (S.intState===1 && S.intT<=0){ S.intState=0; S.intT=(30+120*Math.random())/(0.3+F.inter); } if (S.intState===1) pumpOn=false; } else S.intState=0; /* 桨距指令 */ let rate=0; if (S.mode==='reg'){ S.regPh+=dt; S.thCmd = 3.2+2.4*Math.sin(2*Math.PI*S.regPh/300)+0.9*Math.sin(2*Math.PI*S.regPh/67); rate = Math.max(-3,Math.min(3,(S.thCmd-S.th)/2.5)); // 实测量级: 平均|rate|≈0.08°/s ⇒ 变桨耗油≈1.2 L/min } else if (S.mode==='close'){ rate = S.th<82?3:0; } else if (S.mode==='open'){ rate = S.th>0?-3:0; } if (S.mode!=='estop'){ if (S.p < K.driveP+8) rate=0; // 无压则动不了 S.th = Math.max(0,Math.min(82,S.th+rate*dt)); } const qPitch = Math.abs(rate)*K.LpD; // L/s(进给由 HP 供) /* 阀 25 滞环 + 延迟 */ if (S.mode==='estop' || !S.power){ S.v25cmd=false; } else if (!S.v25cmd && S.p<=K.pLoad){ S.v25cmd=true; S.delayT=K.dLoad; } else if ( S.v25cmd && S.p>=K.pUnload){ S.v25cmd=false; S.delayT=K.dUnload; } if (S.delayT>0){ S.delayT-=dt; } else if (S.v25 !== S.v25cmd){ S.v25 = S.v25cmd; if (S.v25){ S.loadStart=S.t; } else if (S.loadStart!=null){ // 一个循环完成 const tl=S.t-S.loadStart, per=S.t-S.lastUnload; S.lastVc=tl*K.Qp*F.pump; S.lastPer=per; S.cyc.push({t:S.t, tl, per}); S.lastUnload=S.t; if (S.cyc.length>4000) S.cyc.shift(); } } /* 压力动力学(单节点) */ const Qin = (S.v25&&pumpOn) ? K.Qp*F.pump : 0; const C = Ccharge(S.p); if (S.mode!=='estop'){ S.p += (Qin - leak - qPitch)/C*dt; if (S.p>K.pRelief) S.p=K.pRelief; if (S.p<2) S.p=2; } /* 轮毂侧显示:健康跟随 vs 棘轮锁定, 按耦合健康度 h 连续混合 */ if (S.mode!=='estop'){ const h = Math.max(0, Math.min(1, (Ccharge(230)-0.006)/(0.055-0.006))); if (S.p >= S.ratchet) S.ratchet=S.p; // 118 单向: 峰值被锁 else S.ratchet = Math.max(S.p, S.ratchet - (0.15 + qPitch/0.006)*dt); const pHealthy = S.v25 ? S.p-2 : Math.min(244, S.p+26); // 实测: 健康台叶片 p99=244, 中位=站侧+26 S.pH = h*pHealthy + (1-h)*S.ratchet; } /* 紧急顺桨(全阀失电, 轮毂106→103→121→缸;70号不在此路, 其失效不碍顺桨) */ if (S.mode==='estop' && S.estop && !S.estop.done){ const e=S.estop; // 气腔压力: 等温 p·V=p0·V0. Vgas 随放油增大, 单调, 不会发散. const alive = (F.p0H>=0.5); let pAcc = alive ? (K.V0H*F.p0H)/Math.max(S.Vgas, 1) : Math.max(2, S.pH*Math.exp(-e.dV/8)); if (!isFinite(pAcc)) pAcc=2; const dp = pAcc - K.driveP; const Q = (S.th<82 && dp>0.5) ? 3*orifQ(K.d121, dp) : 0; S.th = Math.min(82, S.th + (Q/K.LpD)*dt); if (alive) S.Vgas = Math.min(K.V0H, S.Vgas + Q*dt); e.dV += Q*dt; e.pMin=Math.min(e.pMin,pAcc); S.pH=pAcc; S.p=Math.max(4,S.p-8*dt); e.stallT = (Q<=0.002) ? (e.stallT||0)+dt : 0; if (S.th>=81.9){ e.done=true; e.ok=true; e.tEnd=S.t; showVerdict(); } else if (e.stallT>2.5){ e.done=true; e.ok=false; e.tEnd=S.t; showVerdict(); } else if (S.t-e.t0>180){ e.done=true; e.ok=false; e.tEnd=S.t; showVerdict(); } // 硬超时兜底 } /* NaN 断路器: 任何一步失稳立即复位, 防止渲染层死循环 */ if (!isFinite(S.p)||!isFinite(S.pH)||!isFinite(S.th)){ S.p=220; S.pH=238; S.th=Math.max(0,Math.min(82,S.th||2)); S.ratchet=238; S.Vgas=0; if(S.estop){S.estop.done=true;S.estop.ok=false;} } /* 油温 (duty 每 0.5 s 才重算一次, 避免每步扫全 cyc 数组) */ S.dutyAcc=(S.dutyAcc||0)+dt; if (S.dutyC==null || S.dutyAcc>=0.5){ S.dutyAcc=0; S.dutyC=dutyNow(); } const Pheat = S.dutyC*K.Qp*F.pump*220/10 + (F.leak-1)*0.5; // kW const Ttar = K.T0 + Math.max(0,(Pheat-1.2))*K.TperKW; S.oilT += (Ttar-S.oilT)/K.Ttau*dt; /* 采样 */ S.acc+=dt; if (S.acc>=0.25){ S.acc=0; S.buf.push({t:S.t,p:S.p,pH:S.pH,th:S.th,v:S.v25?1:0}); if (S.buf.length>4800) S.buf.shift(); } } function dutyNow(){ const w=S.cyc.filter(c=>c.t>S.t-600); if (!w.length) return S.v25?0.5:0.08; const span=Math.min(600, Math.max(60, S.t - (S.cyc[0]?.t ?? 0) + 60)); return Math.min(0.95, w.reduce((a,c)=>a+c.tl,0)/span); } /* ================= 指标 ================= */ const MEAS_H={vc:'4.9',per:'96',cnt:'6',band:'50',duty:'7.5',oil:'38',ov:'0.0'}; const MEAS_8={vc:'0.50',per:'2.6',cnt:'228',band:'135',duty:'32.3',oil:'51',ov:'98.3'}; function metrics(){ const w=S.cyc.filter(c=>c.t>S.t-600); const pb=S.buf.filter(b=>b.t>S.t-600); const band = pb.length? (Math.max(...pb.map(b=>b.p))-Math.min(...pb.map(b=>b.p))) : 0; const ov = pb.length? pb.filter(b=>b.pH>246).length/pb.length*100 : 0; const cnt = w.length? Math.round(w.length*600/Math.min(600,Math.max(60,S.t))) : 0; const vc = S.lastVc!=null? S.lastVc.toFixed(2):'—'; const per= S.lastPer!=null? S.lastPer.toFixed(1):'—'; return [ ['每次进油量 L', vc, MEAS_H.vc, MEAS_8.vc], ['充能周期 s', per, MEAS_H.per, MEAS_8.per], ['加载次数 /10min', cnt||'—', MEAS_H.cnt, MEAS_8.cnt], ['压力带宽 bar', band?band.toFixed(0):'—', MEAS_H.band, MEAS_8.band], ['加载占空比 %', (dutyNow()*100).toFixed(1), MEAS_H.duty, MEAS_8.duty], ['液压油温 ℃', S.oilT.toFixed(1), MEAS_H.oil, MEAS_8.oil], ['叶片侧>246 %', ov.toFixed(1), MEAS_H.ov, MEAS_8.ov], ]; } function pistonRows(){ const inEstop = S.mode==='estop' && F.p0H>=0.5 && S.Vgas>0; const Vg70 = K.V70*gasFrac(S.p, F.stuck?0:F.p070); const VgH = inEstop ? S.Vgas : K.V0H*gasFrac(S.pH, F.p0H); const pg70 = (F.p070<0.5||F.stuck)? '—' : (S.p>F.p070? S.p.toFixed(0):F.p070.toFixed(0)); const pgH = (F.p0H<0.5)? '—' : (inEstop? (K.V0H*F.p0H/Math.max(VgH,1e-6)).toFixed(0) : (S.pH>F.p0H? S.pH.toFixed(0):F.p0H.toFixed(0))); return [ ['70号 预充 / 气压', (F.stuck?'卡滞':F.p070.toFixed(0))+' / '+pg70+' bar'], ['70号 储油(10 L 罐)', (K.V70-Vg70).toFixed(1)+' L'], ['106 预充 / 气压', F.p0H.toFixed(0)+' / '+pgH+' bar'], ['106 储油(6 只合计)', (K.V0H-VgH).toFixed(1)+' L'], ['等效容变 C(充能)', Ccharge(S.p).toFixed(4)+' L/bar'], ['本循环吞吐 ΔV', S.lastVc!=null? S.lastVc.toFixed(2)+' L':'—'], ['顺桨需求(压超速/全程)', '5.3 / 21.7 L'], ]; } /* ================= 绘制 ================= */ const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim(); function fit(cv){ const d=window.devicePixelRatio||1, w=cv.clientWidth; if(!cv.dataset.h) cv.dataset.h=cv.getAttribute('height'); // 设计高度只记一次 const asp=+cv.dataset.aspect||0; // aspect: 高=宽×aspect(回路图铺满不留白) /* 迟滞死区+限频: 宽度变化>16px 且距上次重设>500ms 才重设画布 — 每帧重设会与滚动条出没形成布局振荡, Chrome 主线程被打死(2026-08-21 实测根因); Safari 滚动条不占宽故无感。绘制端使用画布实际尺寸, CSS 拉伸差≤16px 无感。 */ const lw=+(cv.dataset.lw||0), lt=+(cv.dataset.lt||0), tn=performance.now(); if (w>0 && (!lw || (Math.abs(w-lw)>16 && tn-lt>500))){ const h=asp? Math.round(w*asp) : +cv.dataset.h; if (cv.width!==Math.round(w*d)||cv.height!==Math.round(h*d)){ cv.width=Math.round(w*d); cv.height=Math.round(h*d); cv.style.height=h+'px'; } cv.dataset.lw=w; cv.dataset.lt=tn; } const g=cv.getContext('2d'); g.setTransform(d,0,0,d,0,0); return [g, cv.width/d, cv.height/d]; } /* Safari<16.4 无 canvas roundRect -> 手绘圆角 */ function rr(g,x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y); g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); } /* Safari canvas fillStyle 不认 color-mix -> JS 混色 */ function hex2rgb(h){ h=h.trim(); if(h[0]==='#'){const v=parseInt(h.slice(1),16); return [(v>>16)&255,(v>>8)&255,v&255];} const m=h.match(/\d+/g)||[240,240,240]; return [+m[0],+m[1],+m[2]]; } function mix(a,b,t){ const A=hex2rgb(a),B=hex2rgb(b); return 'rgb('+Math.round(A[0]*t+B[0]*(1-t))+','+Math.round(A[1]*t+B[1]*(1-t))+','+Math.round(A[2]*t+B[2]*(1-t))+')'; } let dashOff=0; function pcol(p){ // 压力→红色深浅 (JS 混色, 兼容 Safari canvas) const a=Math.max(.12,Math.min(1,p/260)); return mix(css('--hp'), css('--panel2'), a); } function seg(g,pts,color,wd,flow){ g.strokeStyle=color; g.lineWidth=wd; g.lineJoin='round'; g.beginPath(); pts.forEach((p,i)=>i?g.lineTo(p[0],p[1]):g.moveTo(p[0],p[1])); g.stroke(); if (flow>0.02 && !matchMedia('(prefers-reduced-motion: reduce)').matches){ g.save(); g.strokeStyle=css('--bg'); g.lineWidth=Math.max(1.4,wd-2.4); g.setLineDash([3,9]); g.lineDashOffset=-dashOff*Math.min(6,flow*8); g.beginPath(); pts.forEach((p,i)=>i?g.lineTo(p[0],p[1]):g.moveTo(p[0],p[1])); g.stroke(); g.restore(); } } function label(g,x,y,t,c,sz){ g.fillStyle=c||css('--sub'); g.font=(sz||10.5)+'px ui-monospace,Menlo,monospace'; g.fillText(t,x,y); } function box(g,x,y,w,h,id){ g.strokeStyle=css('--sub'); g.lineWidth=1; g.strokeRect(x,y,w,h); if(id){ g.font='9.5px ui-monospace,monospace'; g.fillStyle=css('--sub'); g.fillText(id,x,y-3);} } function drawCircuit(){ const [g,W,H]=fit(cv); g.clearRect(0,0,W,H); const s=W/1040; g.save(); g.translate(0,(H-520*s)/2); g.scale(s,s); const HP=pcol(S.p), HPh=pcol(S.pH), LP=css('--lp'), ink=css('--ink'), sub=css('--sub'); const Qflow=(S.v25&&S.power&&S.mode!=='estop'&&S.intState!==1)?1:0; const byp=(!S.v25&&S.power&&S.mode!=='estop'&&S.intState!==1)?1:0; const pitch=(S.mode!=='estop'&&Math.abs(S.th-(S.buf.length?S.buf[S.buf.length-1].th:S.th))>0)?0.4: (S.mode==='close'||S.mode==='open')?0.8:(S.mode==='reg'?0.3:0); const est=(S.mode==='estop'&&S.estop&&!S.estop.done)?1:0; /* ---- 油箱 & 泵 ---- */ g.strokeStyle=sub; g.strokeRect(60,455,180,42); label(g,66,449,'油箱 23 · 400 L'); // 电机+双泵 g.beginPath(); g.arc(110,420,13,0,7); g.stroke(); label(g,104,424,'M',ink,11); g.beginPath(); g.arc(146,420,13,0,7); g.stroke(); g.fillStyle=ink; g.beginPath(); g.moveTo(146,411); g.lineTo(152,424); g.lineTo(140,424); g.closePath(); g.fill(); label(g,310,464,'25.4 ccm · 1450 rpm ⇒ 36.8 L/min','',9.5); label(g,310,446,S.power&&S.intState!==1?'泵连续运转':'泵停',S.power&&S.intState!==1?css('--ok'):css('--hp'),10.5); seg(g,[[146,433],[146,455]],LP,3,0.3); // 吸油 seg(g,[[146,407],[146,352]],HP,4,Qflow||byp?1:0); // 泵出口 /* ---- 25 号卸荷阀 ---- */ seg(g,[[146,352],[240,352]],HP,3.5,byp); box(g,240,338,44,28,'25'); g.fillStyle=S.v25?css('--hp'):css('--chipbg'); g.fillRect(285,341,10,22); label(g,240,332,S.v25?'得电=封闭(加载)':'失电=泄回(卸荷)',S.v25?css('--hp'):sub,10); seg(g,[[284,366],[284,455]].map(p=>[p[0]-22,p[1]]),LP,3.5,byp); // 回油箱 /* 12 单向阀 */ seg(g,[[146,352],[146,318]],HP,4,Qflow); g.fillStyle=ink; g.beginPath(); g.moveTo(140,318); g.lineTo(152,318); g.lineTo(146,306); g.closePath(); g.fill(); label(g,156,315,'12'); /* 滤芯 11 */ seg(g,[[146,306],[146,282]],HP,4,Qflow); g.save(); g.translate(146,270); g.rotate(Math.PI/4); g.strokeRect(-9,-9,18,18); g.restore(); label(g,160,272,'11 β25µm'); /* 溢流 10 */ seg(g,[[146,258],[146,196]],HP,4,Qflow); seg(g,[[146,232],[210,232]],HP,2.5,S.p>=K.pRelief?1:0); box(g,210,222,26,20,'10'); label(g,240,236,'280 bar',sub); /* ---- 泵站 70 号手刹蓄能器 10 L(ø7.0 节流 + 20×2.5 管, 12 之后同段) ---- */ const a7x=64,a7y=210,a7w=38,a7h=62, f70=gasFrac(S.p, F.stuck?0:F.p070); seg(g,[[a7x+a7w/2,a7y+a7h],[a7x+a7w/2,258],[146,258]],HP,3,0); g.strokeStyle=ink; g.lineWidth=1.6; g.strokeRect(a7x,a7y,a7w,a7h); const g7=Math.max(2,(a7h-6)*Math.min(1,f70)); g.fillStyle=css('--gas'); g.fillRect(a7x+1.5,a7y+1.5,a7w-3,g7); g.fillStyle=sub; g.fillRect(a7x+1.5,a7y+1.5+g7,a7w-3,4); g.fillStyle=css('--hp'); g.globalAlpha=.75; g.fillRect(a7x+1.5,a7y+5.5+g7,a7w-3,a7h-7-g7); g.globalAlpha=1; if (F.stuck){ g.strokeStyle=css('--hp'); g.beginPath(); g.moveTo(a7x,a7y); g.lineTo(a7x+a7w,a7y+a7h); g.moveTo(a7x+a7w,a7y); g.lineTo(a7x,a7y+a7h); g.stroke(); } label(g,36,a7y-8,'70 · 10 L p₀='+(F.stuck?'卡滞':F.p070.toFixed(0)),ink,10); label(g,36,a7y+a7h+14,'近端主缓冲','',9); /* 传感器 17 */ g.beginPath(); g.arc(180,196,15,0,7); g.strokeStyle=sub; g.stroke(); const a0=Math.PI*.75+S.p/300*Math.PI*1.5; g.strokeStyle=ink; g.beginPath(); g.moveTo(180,196); g.lineTo(180+11*Math.cos(a0),196+11*Math.sin(a0)); g.stroke(); label(g,166,177,'17',sub); label(g,150,166,S.p.toFixed(0)+' bar',ink,15); label(g,150,152,'泵站侧 (SCADA 液压压力)','',9.5); /* ---- P HUB → 旋转接头 → 118 ---- */ seg(g,[[146,196],[146,120],[330,120]],HP,4,Qflow); g.strokeStyle=sub; g.beginPath(); g.arc(352,120,16,-1.2,1.2); g.stroke(); g.beginPath(); g.arc(362,120,16,Math.PI-1.2,Math.PI+1.2); g.stroke(); label(g,332,96,'旋转接头'); seg(g,[[378,120],[430,120]],HP,4,Qflow); g.fillStyle=ink; g.beginPath(); g.moveTo(430,113); g.lineTo(430,127); g.lineTo(444,120); g.closePath(); g.fill(); label(g,412,72,'118 单向',sub); label(g,390,87,'← 泵→轮毂 单向 · 棘轮之源','',9); /* ---- 轮毂总管 ---- */ seg(g,[[444,120],[980,120]],HPh,4,Qflow||est?1:0); /* 传感器107 */ g.beginPath(); g.arc(505,158,15,0,7); g.strokeStyle=sub; g.stroke(); const a1=Math.PI*.75+S.pH/300*Math.PI*1.5; g.strokeStyle=ink; g.beginPath(); g.moveTo(505,158); g.lineTo(505+11*Math.cos(a1),158+11*Math.sin(a1)); g.stroke(); seg(g,[[505,143],[505,120]],HPh,2.5,0); label(g,580,151,'107',sub); label(g,580,168,S.pH.toFixed(0)+' bar',ink,14); label(g,580,184,'叶片侧 (桨叶油压)','',9.5); /* ---- 蓄能器 106(合并示意) ---- */ const ax=590,ay=28,aw=54,ah=86; seg(g,[[ax+aw/2,ay+ah],[ax+aw/2,120]],HPh,3.5,Qflow*0.7); g.strokeStyle=ink; g.lineWidth=1.6; g.strokeRect(ax,ay,aw,ah); const gf=gasFrac(S.mode==='estop'&&F.p0H>=0.5? K.V0H*F.p0H/Math.max(S.Vgas,1e-6):S.pH, F.p0H); const gh=Math.max(2,(ah-8)*Math.min(1,(S.mode==='estop'&&S.Vgas>0&&F.p0H>=0.5)?S.Vgas/K.V0H:gf)); g.fillStyle=css('--gas'); g.fillRect(ax+1.5,ay+1.5,aw-3,gh); g.fillStyle=sub; g.fillRect(ax+1.5,ay+1.5+gh,aw-3,5); // 活塞 g.fillStyle=css('--hp'); g.globalAlpha=.75; g.fillRect(ax+1.5,ay+6.5+gh,aw-3,ah-8-gh); g.globalAlpha=1; label(g,ax-4,ay-8,'106 · 6×30 L 合并示意 · 顺桨主源 · p₀='+F.p0H.toFixed(0),ink,10); /* ---- 比例阀 102 → 缸 ---- */ seg(g,[[700,120],[700,206]],HPh,3.5,pitch); box(g,672,206,56,30,'102'); const pv=S.mode==='estop'?'—':(rateDir()>0?'b 位':rateDir()<0?'a 位':'中位'); label(g,676,226,'比例阀 '+pv,ink,10); seg(g,[[728,221],[790,221]],HPh,3,pitch); /* 缸 */ g.strokeStyle=ink; g.strokeRect(790,204,120,34); const rod=790+18+(S.th/82)*76; g.fillStyle=sub; g.fillRect(790+2,206,rod-792,30); g.strokeStyle=ink; g.beginPath(); g.moveTo(rod,221); g.lineTo(rod+56,221); g.stroke(); label(g,792,198,'124/125 · ø125/90×611 ×2缸×3叶',sub,9.5); /* 叶片角 */ g.save(); g.translate(952,221); g.rotate(-S.th*Math.PI/180); g.fillStyle=css('--lp'); g.globalAlpha=.85; g.beginPath(); g.ellipse(0,0,34,9,0,0,7); g.fill(); g.globalAlpha=1; g.restore(); label(g,928,262,'θ = '+S.th.toFixed(1)+'°',ink,14); /* ---- 紧急旁路 103→121 ---- */ seg(g,[[560,120],[560,300],[770,300],[770,238]],est?HPh:css('--grid'),3, est); box(g,596,288,40,24,'103'); label(g,600,304,'失电=通',est?ink:sub,9.5); // 节流孔 121 g.strokeStyle=est?ink:sub; g.beginPath(); g.moveTo(690,292); g.lineTo(702,300); g.lineTo(690,308); g.moveTo(718,292); g.lineTo(706,300); g.lineTo(718,308); g.stroke(); label(g,682,324,'121 ø2.4 顺桨限速',est?css('--hp'):sub,10); /* ---- 回油 ---- */ seg(g,[[700,236],[700,398],[146,398]],LP,3.5,pitch||est?0.6:0.15); g.strokeStyle=sub; g.beginPath(); g.moveTo(640,392); g.lineTo(650,398); g.lineTo(640,404); g.moveTo(664,392); g.lineTo(654,398); g.lineTo(664,404); g.stroke(); label(g,628,416,'116/122 ø1.8 回油',sub,9.5); seg(g,[[146,398],[146,455]],LP,3.5,0.3); label(g,955,114,'P',sub); label(g,60,392,'T',sub); /* 模式标注 */ const mtxt={reg:'运行调节:比例阀 102 微调;泵站 70 号=滞环循环的近端主缓冲,轮毂 106 经 118 弱参与', close:'收桨:102 换向 b 位,需 47 L/min > 泵 36.8 ⇒ 蓄能器补差额', open:'开桨:102 换向 a 位(116 同时得电)', estop:'紧急顺桨:全阀失电 → 轮毂 106 经 103→121 推缸(70 号不在此路,其失效不碍顺桨)'}; document.getElementById('circuit-mode-caption').textContent=mtxt[S.mode]; if (S.intState===1) document.getElementById('circuit-mode-caption').textContent+=' · ⚠ 泵间歇失供中…'; g.restore(); dashOff+=1; } function rateDir(){ if(S.mode==='close')return 1; if(S.mode==='open')return -1; const d=S.thCmd-S.th; return Math.abs(d)>.1?Math.sign(d):0; } /* 活塞特写:双罐并排(左=泵站70号, 右=轮毂106单只) */ function drawPiston(){ const [g,W,H]=fit(pv); g.clearRect(0,0,W,H); const ink=css('--ink'), sub=css('--sub'); label(g,14,18,'蓄能器剖面 · 泵站70号 vs 轮毂106',ink,12); const y=44, h=142, w=64; function tank(x, frac, ttl, cap, stuck){ label(g,x-4,y-8,ttl,ink,10); g.strokeStyle=ink; g.lineWidth=2; rr(g,x,y,w,h,8); g.stroke(); const gh=Math.max(3,(h-10)*Math.min(1,frac)); g.save(); rr(g,x+2,y+2,w-4,h-4,6); g.clip(); g.fillStyle=css('--gas'); g.fillRect(x,y,w,gh+3); for(let i=7;i<gh;i+=12){ g.strokeStyle='rgba(0,0,0,.10)'; g.beginPath(); g.moveTo(x+5,y+i); g.lineTo(x+w-5,y+i+3); g.stroke(); } g.fillStyle=sub; g.fillRect(x,y+gh+1,w,8); g.fillStyle=css('--hp'); g.globalAlpha=.8; g.fillRect(x,y+gh+9,w,h-gh-9); g.globalAlpha=1; g.restore(); if (stuck){ g.strokeStyle=css('--hp'); g.lineWidth=2.5; g.beginPath(); g.moveTo(x+6,y+gh-14); g.lineTo(x+w-6,y+gh+22); g.moveTo(x+w-6,y+gh-14); g.lineTo(x+6,y+gh+22); g.stroke(); } label(g,x-4,y+h+13,cap,sub,9); } tank(24, gasFrac(S.p, F.stuck?0:F.p070), '70号 10L/'+(F.stuck?'卡滞':F.p070.toFixed(0)), '泵压主管 · 近端缓冲', F.stuck); const inEstop=S.mode==='estop'&&F.p0H>=0.5&&S.Vgas>0; tank(122, inEstop? Math.min(1,S.Vgas/K.V0H) : gasFrac(S.pH,F.p0H), '106单只 30L/'+F.p0H.toFixed(0), '叶片块×6 · 顺桨主源', false); label(g,200,y+8,'黄=氮气',css('--gas'),9.5); label(g,200,y+22,'灰=活塞',sub,9.5); label(g,200,y+36,'红=油侧',css('--hp'),9.5); // 吞吐条 + 三态要点 const by=y+h+30, bw=W-40; label(g,20,by,'本循环吞吐 ΔV '+(S.lastVc!=null?S.lastVc.toFixed(2):'—')+' L(健康 4.9 · 8# 0.50)',ink,11); g.strokeStyle=css('--grid'); g.strokeRect(20,by+7,bw,12); const v=Math.min(6,S.lastVc??0); g.fillStyle=css('--hp'); g.fillRect(20,by+7,bw*v/6,12); label(g,20,by+36,'70号失氮 → 循环签名剧变(快循环/宽带宽)· 顺桨不受影响',sub,9.5); label(g,20,by+52,'106失氮 → 循环仅中度变化 · 失电顺桨失去能量源',sub,9.5); label(g,20,by+68,'双失效 → SCADA 签名与单独70号失效不可分辨 + 顺桨失败',sub,9.5); label(g,20,by+84,'气体定律 p·V=p₀·V₀(等温)· 气少=硬弹簧 · 无气=钢块',sub,9.5); } /* 走线图 */ function chart(cv,rows,ylo,yhi,fmt,refs,title){ const [g,W,H]=fit(cv); g.clearRect(0,0,W,H); const L=44,R=10,T=22,B=18, ink=css('--ink'), sub=css('--sub'); label(g,L,14,title,ink,11.5); const win=120, t1=S.t, t0=t1-win; const X=t=>L+(t-t0)/win*(W-L-R), Y=v=>T+(1-(v-ylo)/(yhi-ylo))*(H-T-B); g.strokeStyle=css('--grid'); for(let i=0;i<=4;i++){ const v=ylo+(yhi-ylo)*i/4; g.beginPath(); g.moveTo(L,Y(v)); g.lineTo(W-R,Y(v)); g.stroke(); label(g,4,Y(v)+3,fmt(v),sub,9.5); } (refs||[]).forEach(r=>{ g.strokeStyle=r[2]; g.setLineDash([5,4]); g.beginPath(); g.moveTo(L,Y(r[0])); g.lineTo(W-R,Y(r[0])); g.stroke(); g.setLineDash([]); label(g,W-R-58,Y(r[0])-3,r[1],r[2],9); }); rows.forEach(rw=>{ g.strokeStyle=rw.c; g.lineWidth=rw.w||1.6; g.beginPath(); let pen=false; for(const b of S.buf){ if(b.t<t0)continue; const v=rw.f(b); if(v==null){pen=false;continue;} const x=X(b.t),y=Y(Math.max(ylo,Math.min(yhi,v))); pen?g.lineTo(x,y):g.moveTo(x,y); pen=true; } g.stroke(); if(S.buf.length){ const last=rw.f(S.buf[S.buf.length-1]); if(last!=null) label(g,W-R-46,Y(Math.max(ylo,Math.min(yhi,last)))-5,rw.n+' '+fmt(last),rw.c,9.5); } }); } /* ================= 判定与 UI ================= */ function showVerdict(){ const e=S.estop, el=document.getElementById('verdict'); if(!e||!e.done) return; const dt=(e.tEnd-e.t0).toFixed(1); if(e.ok){ el.className='verdict ok'; const n70=(F.p070<90||F.stuck)? '当前 70 号已注入失效但顺桨仍完成 —— 顺桨主源是轮毂 106(与数据窗 4 次停电全部安全顺桨的实证一致)。' : '等温理想下界 —— 低预充“勉强完成”不代表实机安全,以现场实测为准。'; el.innerHTML=`✓ 顺桨完成:82° 用时 <b class="mono">${dt} s</b> · 放油 <b class="mono">${e.dV.toFixed(1)} L</b> · 最低压 <b class="mono">${e.pMin.toFixed(0)} bar</b><br><span style="color:var(--sub);font-size:12px">${n70}</span>`; } else { el.className='verdict bad'; el.innerHTML=`✗ 顺桨失败:卡滞在 <b class="mono">${S.th.toFixed(0)}°</b>(用时 ${dt} s)· 已放 ${e.dV.toFixed(1)} L · 压力 ${e.pMin.toFixed(0)} bar &lt; 驱动需求 ${K.driveP} bar<br><span style="color:var(--sub);font-size:12px">轮毂 106 失氮 ⇒ 失电顺桨无源。SCADA 充能循环判据对 106 几乎不敏感(循环仅中度变化)——覆盖它的是机组自检容量检查(UP03284 组);两套监测互补,缺一即盲</span>`; } } function setMode(m){ S.mode=m; document.querySelectorAll('#m-reg,#m-close,#m-open,#m-estop') .forEach(b=>b.classList.remove('on')); document.getElementById({reg:'m-reg',close:'m-close',open:'m-open',estop:'m-estop'}[m]).classList.add('on'); const el=document.getElementById('verdict'); if(m==='estop'){ S.estop={t0:S.t,done:false,dV:0,pMin:S.pH}; S.Vgas=(F.p0H>=0.5)? Math.max(K.V0H*F.p0H/K.pRelief, K.V0H*F.p0H/Math.max(S.pH,F.p0H+1)) : 0; el.className='verdict'; el.style.display='none'; } else { el.className='verdict'; S.estop=null; } } const $=id=>document.getElementById(id); $('m-reg').onclick=()=>setMode('reg'); $('m-close').onclick=()=>setMode('close'); $('m-open').onclick=()=>setMode('open'); $('m-estop').onclick=()=>setMode('estop'); $('pw').onclick=()=>{ S.power=!S.power; $('pw').textContent=S.power?'有电':'失电'; $('pw').classList.toggle('on',S.power); }; $('spd').oninput=e=>{ $('spdv').textContent=e.target.value+'×'; }; function bind(id,vid,set,fmt){ $(id).oninput=e=>{ set(+e.target.value); $(vid).textContent=fmt(+e.target.value); }; } bind('s-p070','v-p070',v=>F.p070=v,v=>v+' bar'); bind('s-p0H','v-p0H',v=>F.p0H=v,v=>v+' bar'); bind('s-leak','v-leak',v=>F.leak=v/10,v=>(v/10).toFixed(1)+'×'); bind('s-pump','v-pump',v=>F.pump=v/100,v=>v+'%'); bind('s-int','v-int',v=>F.inter=v/100,v=>v?(v+'%'):'关'); $('s-stuck').onchange=e=>F.stuck=e.target.checked; const PRESETS={ healthy: {p070:180,p0H:125,leak:10,pump:100,int:0,stuck:false}, early: {p070:90, p0H:125,leak:10,pump:100,int:0,stuck:false}, collapse:{p070:1, p0H:125,leak:10,pump:100,int:0,stuck:false}, hubfail: {p070:180,p0H:1, leak:10,pump:100,int:0,stuck:false}, bothfail:{p070:1, p0H:1, leak:10,pump:100,int:0,stuck:false}, leak: {p070:180,p0H:125,leak:22,pump:100,int:0,stuck:false}, pumpint: {p070:180,p0H:125,leak:10,pump:100,int:55,stuck:false}, stuck: {p070:180,p0H:125,leak:10,pump:100,int:0,stuck:true}, }; document.querySelectorAll('.presets button').forEach(b=>b.onclick=()=>{ const p=PRESETS[b.dataset.p]; $('s-p070').value=p.p070; F.p070=p.p070; $('v-p070').textContent=p.p070+' bar'; $('s-p0H').value=p.p0H; F.p0H=p.p0H; $('v-p0H').textContent=p.p0H+' bar'; $('s-leak').value=p.leak; F.leak=p.leak/10; $('v-leak').textContent=(p.leak/10).toFixed(1)+'×'; $('s-pump').value=p.pump; F.pump=p.pump/100; $('v-pump').textContent=p.pump+'%'; $('s-int').value=p.int; F.inter=p.int/100; $('v-int').textContent=p.int?(p.int+'%'):'关'; $('s-stuck').checked=p.stuck; F.stuck=p.stuck; S.cyc.length=0; S.lastVc=null; S.lastPer=null; S.ratchet=S.pH; }); /* ================= 主循环 ================= */ const cv=$('cv'), pv=$('pv'); let last=performance.now(), carry=0; /* 表格 DOM 低频差量更新:每帧 innerHTML 重写会把翻译/监控类浏览器扩展的 MutationObserver 打爆导致整页无响应(Chrome+扩展实测)。4 Hz + 无变化不写。 */ let uiAcc=1, mxHtml='', ptHtml=''; function frame(now){ /* rAF 时间戳可早于 performance.now()(macOS26+Chrome151 实测: 首帧 now<last) → 负 dt 会让 while(n--) 从负数无限递减 = 死循环白屏。负值双重钳制。 */ const dtR=Math.max(0, Math.min(0.06,(now-last)/1000)); last=now; const speed=+$('spd').value; carry+=dtR*speed; const DT=0.02; let n=Math.max(0, Math.min(400,Math.floor(carry/DT))); carry-=n*DT; if (carry<0) carry=0; while(n--) step(DT); try{ drawCircuit(); }catch(e){} try{ drawPiston(); }catch(e){} chart($('c1'),[ {n:'叶片侧',c:css('--lp'),w:1.8,f:b=>b.pH}, {n:'泵站侧',c:css('--hp'),w:1.6,f:b=>b.p}], 100,290,v=>v.toFixed(0), [[194,'194 加载',css('--ok')],[246,'246 卸荷',css('--hp')]], '系统压力 bar · 滚动 120 s'); chart($('c2'),[{n:'θ',c:css('--ink'),w:1.8,f:b=>b.th}], -4,92,v=>v.toFixed(0),[[82,'82° 顺桨',css('--sub')]],'桨距角 °'); chart($('c3'),[{n:'25号阀',c:css('--gas'),w:1.6,f:b=>b.v*0.9+0.05}], -0.08,1.15,v=>v>0.9?'得电':(v<0.1?'失电':''),[],'25 号卸荷阀状态(得电=加载)'); uiAcc+=dtR; if (uiAcc>=0.25){ uiAcc=0; const h1=metrics().map(r=> `<tr><td>${r[0]}</td><td><b>${r[1]}</b></td><td style="color:var(--sub)">${r[2]}</td><td style="color:var(--hp)">${r[3]}</td></tr>`).join(''); if (h1!==mxHtml){ mxHtml=h1; $('mx').innerHTML=h1; } const h2=pistonRows().map(r=> `<tr><td style="text-align:left;color:var(--sub)">${r[0]}</td><td><b>${r[1]}</b></td></tr>`).join(''); if (h2!==ptHtml){ ptHtml=h2; $('pistab').innerHTML=h2; } } requestAnimationFrame(frame); } requestAnimationFrame(frame); </script>