tpl-U6_变桨液压仿真台.html.html 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <meta charset="utf-8">
  2. <meta name="viewport" content="width=device-width,initial-scale=1">
  3. <title>变桨液压回路仿真台 · SWT-4.0-130</title>
  4. <meta name="google" content="notranslate">
  5. <style>
  6. :root{
  7. --bg:#F4F6F8; --panel:#FFFFFF; --panel2:#EDF0F3; --ink:#10151B; --sub:#5A6472;
  8. --line:#D8DDE3; --grid:#E4E8EC;
  9. --hp:#C8302B; --lp:#1E7FC2; --gas:#D99A2B; --ok:#2E8B57; --warn:#B7791F;
  10. --chipbg:#EDF0F3; --focus:#1E7FC2;
  11. }
  12. @media (prefers-color-scheme: dark){:root{
  13. --bg:#0D1117; --panel:#151B23; --panel2:#1C242E; --ink:#E8EBEF; --sub:#93A0AD;
  14. --line:#2A3340; --grid:#222B36;
  15. --hp:#E05A50; --lp:#4FA3DC; --gas:#E2AC4A; --ok:#4CAF7D; --warn:#D9A24A;
  16. --chipbg:#1C242E;
  17. }}
  18. :root[data-theme="dark"]{
  19. --bg:#0D1117; --panel:#151B23; --panel2:#1C242E; --ink:#E8EBEF; --sub:#93A0AD;
  20. --line:#2A3340; --grid:#222B36;
  21. --hp:#E05A50; --lp:#4FA3DC; --gas:#E2AC4A; --ok:#4CAF7D; --warn:#D9A24A;
  22. --chipbg:#1C242E;
  23. }
  24. :root[data-theme="light"]{
  25. --bg:#F4F6F8; --panel:#FFFFFF; --panel2:#EDF0F3; --ink:#10151B; --sub:#5A6472;
  26. --line:#D8DDE3; --grid:#E4E8EC;
  27. --hp:#C8302B; --lp:#1E7FC2; --gas:#D99A2B; --ok:#2E8B57; --warn:#B7791F;
  28. --chipbg:#EDF0F3;
  29. }
  30. *{box-sizing:border-box}
  31. body{margin:0;background:var(--bg);color:var(--ink);
  32. font:14px/1.55 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
  33. .mono{font-family:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
  34. font-variant-numeric:tabular-nums;}
  35. header{padding:14px 18px 10px;border-bottom:1px solid var(--line)}
  36. header h1{margin:0;font-size:17px;font-weight:700;letter-spacing:.02em}
  37. header .sub{color:var(--sub);font-size:12px;margin-top:2px}
  38. .bar{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;padding:10px 18px;
  39. border-bottom:1px solid var(--line);background:var(--panel)}
  40. .bar .grp{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
  41. .bar .lab{font-size:11px;color:var(--sub);letter-spacing:.08em;text-transform:uppercase}
  42. button{font:inherit;color:var(--ink);background:var(--chipbg);border:1px solid var(--line);
  43. border-radius:5px;padding:5px 12px;cursor:pointer}
  44. button:hover{border-color:var(--sub)}
  45. button:focus-visible{outline:2px solid var(--focus);outline-offset:1px}
  46. button.on{background:var(--ink);color:var(--bg);border-color:var(--ink)}
  47. button.estop{border-color:var(--hp);color:var(--hp);font-weight:700}
  48. button.estop.on{background:var(--hp);color:#fff}
  49. main{display:grid;grid-template-columns:305px minmax(430px,1fr) 355px;gap:12px;
  50. padding:12px 18px;align-items:start}
  51. .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px}
  52. .card h3{margin:0 0 8px;font-size:12px;letter-spacing:.08em;color:var(--sub);
  53. text-transform:uppercase;font-weight:600}
  54. .fault{margin-bottom:14px}
  55. .fault .name{display:flex;justify-content:space-between;align-items:baseline;font-weight:600;font-size:13px}
  56. .fault .val{color:var(--hp)}
  57. .fault input[type=range]{width:100%;accent-color:var(--hp)}
  58. .fault .hint{font-size:11.5px;color:var(--sub);margin-top:1px}
  59. .presets{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
  60. .presets button{font-size:12px;padding:3px 9px}
  61. table.mx{width:100%;border-collapse:collapse;font-size:12.5px}
  62. table.mx th,table.mx td{padding:3.5px 4px;text-align:right;border-bottom:1px solid var(--grid)}
  63. table.mx th:first-child,table.mx td:first-child{text-align:left}
  64. table.mx thead th{color:var(--sub);font-weight:600;font-size:11px}
  65. canvas{display:block;width:100%;border:1px solid var(--line);border-radius:8px;background:var(--panel)}
  66. .split{display:grid;grid-template-columns:290px 1fr;gap:12px;margin-top:12px}
  67. .verdict{margin-top:10px;padding:9px 12px;border-radius:6px;font-size:13px;display:none}
  68. .verdict.ok{display:block;background:rgba(46,139,87,.12);border:1px solid var(--ok)}
  69. .verdict.bad{display:block;background:rgba(200,48,43,.12);border:1px solid var(--hp)}
  70. .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
  71. .chip{font-size:11.5px;background:var(--chipbg);border:1px solid var(--line);
  72. border-radius:4px;padding:3px 8px;color:var(--sub)}
  73. footer{padding:6px 18px 26px;color:var(--sub);font-size:12px;max-width:1100px}
  74. footer b{color:var(--ink)}
  75. label.tg{display:inline-flex;gap:5px;align-items:center;cursor:pointer;font-size:12.5px}
  76. input[type=checkbox]{accent-color:var(--hp)}
  77. @media (max-width:1240px){main{grid-template-columns:290px 1fr}.right{grid-column:1/-1;
  78. display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px}}
  79. @media (max-width:860px){main{grid-template-columns:1fr}.split{grid-template-columns:1fr}}
  80. @media (prefers-reduced-motion: reduce){.anim{display:none}}
  81. </style><style id="guanlan-pitch-light">
  82. :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}
  83. 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)}}
  84. </style>
  85. <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>
  86. <h1>变桨液压回路仿真台 <span class="mono" style="font-weight:400;color:var(--sub)">SWT-4.0-130 · 图纸 1034384/1034386</span></h1>
  87. <div class="sub">泵持续运转 + 25 号阀滞环卸荷 + <b>双蓄能器</b>(泵站 70 号 10L/180bar 近端缓冲 · 轮毂 106 6×30L/125bar 顺桨主源)—— 参数标定自本场 38 台实测与原厂图纸;分别注入失效,看两者的签名有何不同</div>
  88. </header>
  89. <div class="bar">
  90. <div class="grp"><span class="lab">工况</span>
  91. <button id="m-reg" class="on">运行调节</button>
  92. <button id="m-close">收桨 0→82°</button>
  93. <button id="m-open">开桨 82→0°</button>
  94. <button id="m-estop" class="estop">紧急顺桨</button>
  95. </div>
  96. <div class="grp"><span class="lab">电源</span>
  97. <button id="pw" class="on">有电</button>
  98. </div>
  99. <div class="grp"><span class="lab">倍速</span>
  100. <input id="spd" type="range" min="1" max="40" value="10" style="width:110px">
  101. <span id="spdv" class="mono" style="font-size:12px">10×</span>
  102. </div>
  103. <div class="grp"><span class="lab">预设</span>
  104. <div class="presets">
  105. <button data-p="healthy">健康台</button>
  106. <button data-p="early">70号早期失氮</button>
  107. <button data-p="collapse">70号失效=复现 8#</button>
  108. <button data-p="hubfail">106失氮(假设)</button>
  109. <button data-p="bothfail">双失效(最不利)</button>
  110. <button data-p="leak">复现 25# 泄漏</button>
  111. <button data-p="pumpint">复现 14# 泵间歇</button>
  112. <button data-p="stuck">70号卡滞(假设)</button>
  113. </div>
  114. </div>
  115. </div>
  116. <main>
  117. <div>
  118. <div class="card">
  119. <h3>失效注入</h3>
  120. <div class="fault">
  121. <div class="name"><span>① 泵站70号失氮 <span style="color:var(--sub);font-weight:400">10L/180bar</span></span>
  122. <span class="val mono" id="v-p070">180 bar</span></div>
  123. <input type="range" id="s-p070" min="0" max="180" value="180" step="1">
  124. <div class="hint">180=铭牌 · ≈0–2 匹配 8# 实测(带宽135/周期2.6s) · A 类失效部位(现场证实, 4# 换件先例)</div>
  125. <label class="tg" style="margin-top:4px"><input type="checkbox" id="s-stuck">
  126. 70号活塞卡滞 <span style="color:var(--sub)">— 候选机理(未证实),油气零交换</span></label>
  127. </div>
  128. <div class="fault">
  129. <div class="name"><span>② 轮毂106失氮 <span style="color:var(--sub);font-weight:400">6×30L/125bar</span></span>
  130. <span class="val mono" id="v-p0H">125 bar</span></div>
  131. <input type="range" id="s-p0H" min="0" max="160" value="125" step="1">
  132. <div class="hint">125=铭牌@20℃ · 2026-08 现场气侧实测≈140(夏季温修内,正常) · 拖低=假设情形: 循环仅中度变化、失电顺桨失败</div>
  133. </div>
  134. <div class="fault">
  135. <div class="name"><span>③ 系统泄漏</span><span class="val mono" id="v-leak">1.0×</span></div>
  136. <input type="range" id="s-leak" min="10" max="60" value="10" step="1">
  137. <div class="hint">泄漏基线 2.2 L/min(+调节变桨≈1.0=实测总消耗3.2)· 25# ≈ 2.8×</div>
  138. </div>
  139. <div class="fault">
  140. <div class="name"><span>④ 泵性能</span><span class="val mono" id="v-pump">100%</span></div>
  141. <input type="range" id="s-pump" min="20" max="100" value="100" step="1">
  142. <div class="hint">磨损/容积效率下降 → 加载时间变长</div>
  143. <div class="name" style="margin-top:6px"><span> 间歇失供</span><span class="val mono" id="v-int">关</span></div>
  144. <input type="range" id="s-int" min="0" max="100" value="0" step="1">
  145. <div class="hint">随机掉供数十秒—数分钟 → 复现 14#/32# 的分钟级低压塌陷</div>
  146. </div>
  147. <div class="verdict" id="verdict"></div>
  148. </div>
  149. <div class="card" style="margin-top:12px">
  150. <h3>SCADA 派生指标(滚动 10 min)</h3>
  151. <table class="mx mono">
  152. <thead><tr><th>指标</th><th>仿真</th><th>健康实测</th><th>8# 实测</th></tr></thead>
  153. <tbody id="mx"></tbody>
  154. </table>
  155. </div>
  156. </div>
  157. <div>
  158. <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>
  159. <div class="split">
  160. <canvas id="pv" height="330"></canvas>
  161. <div class="card">
  162. <h3>活塞与进油量 · 逐循环</h3>
  163. <table class="mx mono"><tbody id="pistab"></tbody></table>
  164. <div class="chips">
  165. <span class="chip mono">ΔV = C · Δp</span>
  166. <span class="chip mono">C = 管路 + C70 + 0.095·C106</span>
  167. <span class="chip mono">Cᵢ = Vᵢ·p₀ᵢ / p²</span>
  168. <span class="chip mono">周期 ≈ C·带宽 ÷ 消耗</span>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. <div class="right">
  174. <canvas id="c1" height="200"></canvas>
  175. <canvas id="c2" height="170"></canvas>
  176. <canvas id="c3" height="170"></canvas>
  177. </div>
  178. </main>
  179. <footer>
  180. <b>边界与简化(读图前请先知道):</b>
  181. ① 单节点压力模型 + 「棘轮」叠加显示;双蓄能器按失效部位改判(v47p,2026-08-13 现场证实)的定量闭合参数化:
  182. 泵站 70 号(10L/180bar,ø7.0 节流+20×2.5 管)直连泵压主管<b>全额参与</b>充能循环,
  183. 轮毂 106(6×30L/125bar)经长管+旋转接头+118 单向阀<b>弱耦合</b>(kH=0.095 标定,健康总 C 对齐实测 0.087 L/bar);
  184. 70 号垮塌 → 循环尖峰 → 棘轮抬升 → 118 恒闭 → 轮毂被踢出循环(C 塌到纯管路 ≈0.003,对齐 8# 实测 0.0037)——此连锁按参数化关系建模,非流网求解;
  185. ② <b>三种失效的签名差异(本台的教学核心)</b>:70 号失效 = SCADA 循环签名剧变(吞吐 0.5L/周期 2.6s/带宽 135)而失电顺桨不受影响(数据窗 4 次全场停电全部安全顺桨、零超速 = 实证);
  186. 106 失氮 = 循环仅中度变化(吞吐 ~2L,与个体基线难分辨)而失电顺桨失去能量源 —— SCADA 判据对它几乎盲,覆盖它的是机组自检容量检查(UP03284 组,从轮毂 235bar 起测);
  187. 双失效 = SCADA 上与单独 70 号失效不可分辨 + 顺桨失败。<b>两套监测互补,缺一即盲</b>;
  188. ③ 紧急顺桨用轮毂 106 设计容量 6×30L 与等温气体定律,缸驱动背压取 25 bar(气动力矩未建模)——「低预充仍勉强完成」是理想下界,实机以现场实测为准;70 号对顺桨的贡献(<2L,约 7%)未建模;
  189. ④ 预设「复现 X# 签名」意为让派生指标匹配该台实测,其 p₀ 数值不是实测氮压;「106失氮」是<b>假设情形</b>——本场 106 已实测正常(2026-08 气侧 ≈140 bar)。
  190. 数据源:图纸 1034384/1034386 · 实机参数 UP07200/07228(245/195)· 38 台 SCADA 实测(2025-01→2026-07)· 2026-08-13 现场核实(70 号换件履历 + 106 气侧实测)。
  191. </footer>
  192. <script>
  193. 'use strict';
  194. /* ================= 物理内核(标定值,来源见页脚) ================= */
  195. const K = {
  196. Qp: 36.8/60, // 变桨泵 L/s(25.4ccm×1450rpm)
  197. pLoad: 194, pUnload: 246, pRelief: 280,
  198. dLoad: 0.80, dUnload: 0.14, // 加载/卸荷响应延迟 s(五台反推 0.80±0.03)
  199. V70: 10, p70n: 180, // 泵站 70 号手刹蓄能器 10 L / 180 bar(ø7.0 节流+20×2.5 管直连泵压主管)
  200. V0H: 180, p0Hn: 125, // 轮毂 106 蓄能器 6×30 L / 125 bar(旋转接头+118 之后,顺桨主源)
  201. kH: 0.095, // 轮毂对充能循环的弱耦合系数(标定: 健康总 C 对齐实测 0.087 L/bar)
  202. Cline: 0.0025, // 管路/油柔度残余容变 L/bar(对齐 8# 塌陷签名 0.50L/2.6s/带宽135)
  203. LpD: 0.2642, LpDb: 0.0881, // 总/单叶片 L/度(环形腔 ø125/90×611)
  204. qLeak0: 2.2/60, // 泄漏基线 L/s @220bar (总消耗3.2 = 此项 + 调节变桨≈1.0)
  205. driveP: 25, // 缸驱动背压假设 bar
  206. d121: 2.4, // 顺桨节流孔 mm(每叶片块一只)
  207. rho: 870, Cd: 0.62,
  208. T0: 38, TperKW: 3.2, Ttau: 240, // 油温:+4 K/kW,τ=240 s
  209. };
  210. const orifQ = (mm, dp) => dp<=0 ? 0 :
  211. K.Cd*Math.PI/4*(mm/1000)**2*Math.sqrt(2*dp*1e5/K.rho)*1000; // L/s
  212. /* ================= 状态 ================= */
  213. const S = {
  214. t:0, p:220, pH:238, th:2, mode:'reg', power:true,
  215. v25:false, v25cmd:false, delayT:0, // 阀 25(true=得电=加载)
  216. Vgas:0, // 顺桨期气腔体积(动态)
  217. estop:null, // {t0, done, stall...}
  218. oilT:38, thCmd:2, regPh:0,
  219. intState:0, intT:0, // 间歇失供状态机
  220. cyc:[], lastUnload:0, loadStart:null, lastVc:null, lastPer:null,
  221. buf:[], acc:0, ratchet:238,
  222. };
  223. const F = { p070:180, p0H:125, stuck:false, leak:1.0, pump:1.0, inter:0 };
  224. /* 单罐等温容变 L/bar(V=总容积, p0=预充;p≤p0 活塞顶死, 只剩止点软区) */
  225. function accC(V, p0, p){
  226. if (p0 < 0.5) return 0;
  227. if (p <= p0) return 0.02;
  228. return V*p0/(p*p);
  229. }
  230. /* 70号垮塌 → 尖峰 → 棘轮抬升 → 118 恒闭 → 轮毂被踢出循环(v47p 连锁, 参数化) */
  231. function hubGate(){
  232. return F.stuck ? 0 : Math.max(0, Math.min(1, (F.p070-20)/60));
  233. }
  234. /* 充能循环等效容变 = 管路 + 泵站70号(全额) + 轮毂106(弱耦合×连锁门) */
  235. function Ccharge(p){
  236. let c = K.Cline;
  237. if (!F.stuck) c += accC(K.V70, F.p070, p);
  238. c += K.kH * hubGate() * accC(K.V0H, F.p0H, p);
  239. return c;
  240. }
  241. /* 稳态气腔占比(显示用) */
  242. function gasFrac(p, p0){
  243. if (p0 < 0.5) return 0;
  244. if (p <= p0) return 1;
  245. return Math.min(1, p0/p);
  246. }
  247. /* ================= 仿真步 ================= */
  248. function step(dt){
  249. S.t += dt;
  250. const perEst = Math.max(1.6, S.lastPer ?? 90);
  251. const qCycle = 0.45/perEst; // 阀切换/瞬态每循环开销 ≈0.45 L (健康台 0.3 L/min, 塌陷台 ~10 L/min)
  252. const leak = K.qLeak0*F.leak*(S.p/220) + qCycle;
  253. /* 泵可用性 */
  254. let pumpOn = S.power && S.mode!=='estop';
  255. if (pumpOn && F.inter>0){ // 间歇失供
  256. S.intT -= dt;
  257. if (S.intState===0 && S.intT<=0){ S.intState=1; S.intT=15+F.inter*165*Math.random(); }
  258. else if (S.intState===1 && S.intT<=0){ S.intState=0; S.intT=(30+120*Math.random())/(0.3+F.inter); }
  259. if (S.intState===1) pumpOn=false;
  260. } else S.intState=0;
  261. /* 桨距指令 */
  262. let rate=0;
  263. if (S.mode==='reg'){
  264. S.regPh+=dt;
  265. S.thCmd = 3.2+2.4*Math.sin(2*Math.PI*S.regPh/300)+0.9*Math.sin(2*Math.PI*S.regPh/67);
  266. rate = Math.max(-3,Math.min(3,(S.thCmd-S.th)/2.5)); // 实测量级: 平均|rate|≈0.08°/s ⇒ 变桨耗油≈1.2 L/min
  267. } else if (S.mode==='close'){ rate = S.th<82?3:0; }
  268. else if (S.mode==='open'){ rate = S.th>0?-3:0; }
  269. if (S.mode!=='estop'){
  270. if (S.p < K.driveP+8) rate=0; // 无压则动不了
  271. S.th = Math.max(0,Math.min(82,S.th+rate*dt));
  272. }
  273. const qPitch = Math.abs(rate)*K.LpD; // L/s(进给由 HP 供)
  274. /* 阀 25 滞环 + 延迟 */
  275. if (S.mode==='estop' || !S.power){ S.v25cmd=false; }
  276. else if (!S.v25cmd && S.p<=K.pLoad){ S.v25cmd=true; S.delayT=K.dLoad; }
  277. else if ( S.v25cmd && S.p>=K.pUnload){ S.v25cmd=false; S.delayT=K.dUnload; }
  278. if (S.delayT>0){ S.delayT-=dt; }
  279. else if (S.v25 !== S.v25cmd){
  280. S.v25 = S.v25cmd;
  281. if (S.v25){ S.loadStart=S.t; }
  282. else if (S.loadStart!=null){ // 一个循环完成
  283. const tl=S.t-S.loadStart, per=S.t-S.lastUnload;
  284. S.lastVc=tl*K.Qp*F.pump; S.lastPer=per;
  285. S.cyc.push({t:S.t, tl, per}); S.lastUnload=S.t;
  286. if (S.cyc.length>4000) S.cyc.shift();
  287. }
  288. }
  289. /* 压力动力学(单节点) */
  290. const Qin = (S.v25&&pumpOn) ? K.Qp*F.pump : 0;
  291. const C = Ccharge(S.p);
  292. if (S.mode!=='estop'){
  293. S.p += (Qin - leak - qPitch)/C*dt;
  294. if (S.p>K.pRelief) S.p=K.pRelief;
  295. if (S.p<2) S.p=2;
  296. }
  297. /* 轮毂侧显示:健康跟随 vs 棘轮锁定, 按耦合健康度 h 连续混合 */
  298. if (S.mode!=='estop'){
  299. const h = Math.max(0, Math.min(1, (Ccharge(230)-0.006)/(0.055-0.006)));
  300. if (S.p >= S.ratchet) S.ratchet=S.p; // 118 单向: 峰值被锁
  301. else S.ratchet = Math.max(S.p, S.ratchet - (0.15 + qPitch/0.006)*dt);
  302. const pHealthy = S.v25 ? S.p-2 : Math.min(244, S.p+26); // 实测: 健康台叶片 p99=244, 中位=站侧+26
  303. S.pH = h*pHealthy + (1-h)*S.ratchet;
  304. }
  305. /* 紧急顺桨(全阀失电, 轮毂106→103→121→缸;70号不在此路, 其失效不碍顺桨) */
  306. if (S.mode==='estop' && S.estop && !S.estop.done){
  307. const e=S.estop;
  308. // 气腔压力: 等温 p·V=p0·V0. Vgas 随放油增大, 单调, 不会发散.
  309. const alive = (F.p0H>=0.5);
  310. let pAcc = alive ? (K.V0H*F.p0H)/Math.max(S.Vgas, 1) : Math.max(2, S.pH*Math.exp(-e.dV/8));
  311. if (!isFinite(pAcc)) pAcc=2;
  312. const dp = pAcc - K.driveP;
  313. const Q = (S.th<82 && dp>0.5) ? 3*orifQ(K.d121, dp) : 0;
  314. S.th = Math.min(82, S.th + (Q/K.LpD)*dt);
  315. if (alive) S.Vgas = Math.min(K.V0H, S.Vgas + Q*dt);
  316. e.dV += Q*dt; e.pMin=Math.min(e.pMin,pAcc); S.pH=pAcc; S.p=Math.max(4,S.p-8*dt);
  317. e.stallT = (Q<=0.002) ? (e.stallT||0)+dt : 0;
  318. if (S.th>=81.9){ e.done=true; e.ok=true; e.tEnd=S.t; showVerdict(); }
  319. else if (e.stallT>2.5){ e.done=true; e.ok=false; e.tEnd=S.t; showVerdict(); }
  320. else if (S.t-e.t0>180){ e.done=true; e.ok=false; e.tEnd=S.t; showVerdict(); } // 硬超时兜底
  321. }
  322. /* NaN 断路器: 任何一步失稳立即复位, 防止渲染层死循环 */
  323. if (!isFinite(S.p)||!isFinite(S.pH)||!isFinite(S.th)){
  324. S.p=220; S.pH=238; S.th=Math.max(0,Math.min(82,S.th||2));
  325. S.ratchet=238; S.Vgas=0; if(S.estop){S.estop.done=true;S.estop.ok=false;}
  326. }
  327. /* 油温 (duty 每 0.5 s 才重算一次, 避免每步扫全 cyc 数组) */
  328. S.dutyAcc=(S.dutyAcc||0)+dt;
  329. if (S.dutyC==null || S.dutyAcc>=0.5){ S.dutyAcc=0; S.dutyC=dutyNow(); }
  330. const Pheat = S.dutyC*K.Qp*F.pump*220/10 + (F.leak-1)*0.5; // kW
  331. const Ttar = K.T0 + Math.max(0,(Pheat-1.2))*K.TperKW;
  332. S.oilT += (Ttar-S.oilT)/K.Ttau*dt;
  333. /* 采样 */
  334. S.acc+=dt;
  335. if (S.acc>=0.25){ S.acc=0;
  336. S.buf.push({t:S.t,p:S.p,pH:S.pH,th:S.th,v:S.v25?1:0});
  337. if (S.buf.length>4800) S.buf.shift();
  338. }
  339. }
  340. function dutyNow(){
  341. const w=S.cyc.filter(c=>c.t>S.t-600);
  342. if (!w.length) return S.v25?0.5:0.08;
  343. const span=Math.min(600, Math.max(60, S.t - (S.cyc[0]?.t ?? 0) + 60));
  344. return Math.min(0.95, w.reduce((a,c)=>a+c.tl,0)/span);
  345. }
  346. /* ================= 指标 ================= */
  347. const MEAS_H={vc:'4.9',per:'96',cnt:'6',band:'50',duty:'7.5',oil:'38',ov:'0.0'};
  348. const MEAS_8={vc:'0.50',per:'2.6',cnt:'228',band:'135',duty:'32.3',oil:'51',ov:'98.3'};
  349. function metrics(){
  350. const w=S.cyc.filter(c=>c.t>S.t-600);
  351. const pb=S.buf.filter(b=>b.t>S.t-600);
  352. const band = pb.length? (Math.max(...pb.map(b=>b.p))-Math.min(...pb.map(b=>b.p))) : 0;
  353. const ov = pb.length? pb.filter(b=>b.pH>246).length/pb.length*100 : 0;
  354. const cnt = w.length? Math.round(w.length*600/Math.min(600,Math.max(60,S.t))) : 0;
  355. const vc = S.lastVc!=null? S.lastVc.toFixed(2):'—';
  356. const per= S.lastPer!=null? S.lastPer.toFixed(1):'—';
  357. return [
  358. ['每次进油量 L', vc, MEAS_H.vc, MEAS_8.vc],
  359. ['充能周期 s', per, MEAS_H.per, MEAS_8.per],
  360. ['加载次数 /10min', cnt||'—', MEAS_H.cnt, MEAS_8.cnt],
  361. ['压力带宽 bar', band?band.toFixed(0):'—', MEAS_H.band, MEAS_8.band],
  362. ['加载占空比 %', (dutyNow()*100).toFixed(1), MEAS_H.duty, MEAS_8.duty],
  363. ['液压油温 ℃', S.oilT.toFixed(1), MEAS_H.oil, MEAS_8.oil],
  364. ['叶片侧>246 %', ov.toFixed(1), MEAS_H.ov, MEAS_8.ov],
  365. ];
  366. }
  367. function pistonRows(){
  368. const inEstop = S.mode==='estop' && F.p0H>=0.5 && S.Vgas>0;
  369. const Vg70 = K.V70*gasFrac(S.p, F.stuck?0:F.p070);
  370. const VgH = inEstop ? S.Vgas : K.V0H*gasFrac(S.pH, F.p0H);
  371. const pg70 = (F.p070<0.5||F.stuck)? '—' : (S.p>F.p070? S.p.toFixed(0):F.p070.toFixed(0));
  372. const pgH = (F.p0H<0.5)? '—' : (inEstop? (K.V0H*F.p0H/Math.max(VgH,1e-6)).toFixed(0)
  373. : (S.pH>F.p0H? S.pH.toFixed(0):F.p0H.toFixed(0)));
  374. return [
  375. ['70号 预充 / 气压', (F.stuck?'卡滞':F.p070.toFixed(0))+' / '+pg70+' bar'],
  376. ['70号 储油(10 L 罐)', (K.V70-Vg70).toFixed(1)+' L'],
  377. ['106 预充 / 气压', F.p0H.toFixed(0)+' / '+pgH+' bar'],
  378. ['106 储油(6 只合计)', (K.V0H-VgH).toFixed(1)+' L'],
  379. ['等效容变 C(充能)', Ccharge(S.p).toFixed(4)+' L/bar'],
  380. ['本循环吞吐 ΔV', S.lastVc!=null? S.lastVc.toFixed(2)+' L':'—'],
  381. ['顺桨需求(压超速/全程)', '5.3 / 21.7 L'],
  382. ];
  383. }
  384. /* ================= 绘制 ================= */
  385. const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim();
  386. function fit(cv){ const d=window.devicePixelRatio||1, w=cv.clientWidth;
  387. if(!cv.dataset.h) cv.dataset.h=cv.getAttribute('height'); // 设计高度只记一次
  388. const asp=+cv.dataset.aspect||0; // aspect: 高=宽×aspect(回路图铺满不留白)
  389. /* 迟滞死区+限频: 宽度变化>16px 且距上次重设>500ms 才重设画布 —
  390. 每帧重设会与滚动条出没形成布局振荡, Chrome 主线程被打死(2026-08-21 实测根因);
  391. Safari 滚动条不占宽故无感。绘制端使用画布实际尺寸, CSS 拉伸差≤16px 无感。 */
  392. const lw=+(cv.dataset.lw||0), lt=+(cv.dataset.lt||0), tn=performance.now();
  393. if (w>0 && (!lw || (Math.abs(w-lw)>16 && tn-lt>500))){
  394. const h=asp? Math.round(w*asp) : +cv.dataset.h;
  395. if (cv.width!==Math.round(w*d)||cv.height!==Math.round(h*d)){
  396. cv.width=Math.round(w*d); cv.height=Math.round(h*d); cv.style.height=h+'px'; }
  397. cv.dataset.lw=w; cv.dataset.lt=tn;
  398. }
  399. const g=cv.getContext('2d'); g.setTransform(d,0,0,d,0,0);
  400. return [g, cv.width/d, cv.height/d]; }
  401. /* Safari<16.4 无 canvas roundRect -> 手绘圆角 */
  402. function rr(g,x,y,w,h,r){ g.beginPath(); g.moveTo(x+r,y);
  403. g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r);
  404. g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
  405. /* Safari canvas fillStyle 不认 color-mix -> JS 混色 */
  406. function hex2rgb(h){ h=h.trim(); if(h[0]==='#'){const v=parseInt(h.slice(1),16);
  407. return [(v>>16)&255,(v>>8)&255,v&255];} const m=h.match(/\d+/g)||[240,240,240];
  408. return [+m[0],+m[1],+m[2]]; }
  409. function mix(a,b,t){ const A=hex2rgb(a),B=hex2rgb(b);
  410. 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))+')'; }
  411. let dashOff=0;
  412. function pcol(p){ // 压力→红色深浅 (JS 混色, 兼容 Safari canvas)
  413. const a=Math.max(.12,Math.min(1,p/260));
  414. return mix(css('--hp'), css('--panel2'), a);
  415. }
  416. function seg(g,pts,color,wd,flow){
  417. g.strokeStyle=color; g.lineWidth=wd; g.lineJoin='round'; g.beginPath();
  418. pts.forEach((p,i)=>i?g.lineTo(p[0],p[1]):g.moveTo(p[0],p[1])); g.stroke();
  419. if (flow>0.02 && !matchMedia('(prefers-reduced-motion: reduce)').matches){
  420. g.save(); g.strokeStyle=css('--bg'); g.lineWidth=Math.max(1.4,wd-2.4);
  421. g.setLineDash([3,9]); g.lineDashOffset=-dashOff*Math.min(6,flow*8);
  422. g.beginPath(); pts.forEach((p,i)=>i?g.lineTo(p[0],p[1]):g.moveTo(p[0],p[1]));
  423. g.stroke(); g.restore();
  424. }
  425. }
  426. function label(g,x,y,t,c,sz){ g.fillStyle=c||css('--sub');
  427. g.font=(sz||10.5)+'px ui-monospace,Menlo,monospace'; g.fillText(t,x,y); }
  428. function box(g,x,y,w,h,id){ g.strokeStyle=css('--sub'); g.lineWidth=1;
  429. g.strokeRect(x,y,w,h); if(id){ g.font='9.5px ui-monospace,monospace';
  430. g.fillStyle=css('--sub'); g.fillText(id,x,y-3);} }
  431. function drawCircuit(){
  432. const [g,W,H]=fit(cv); g.clearRect(0,0,W,H);
  433. const s=W/1040;
  434. g.save(); g.translate(0,(H-520*s)/2); g.scale(s,s);
  435. const HP=pcol(S.p), HPh=pcol(S.pH), LP=css('--lp'), ink=css('--ink'), sub=css('--sub');
  436. const Qflow=(S.v25&&S.power&&S.mode!=='estop'&&S.intState!==1)?1:0;
  437. const byp=(!S.v25&&S.power&&S.mode!=='estop'&&S.intState!==1)?1:0;
  438. const pitch=(S.mode!=='estop'&&Math.abs(S.th-(S.buf.length?S.buf[S.buf.length-1].th:S.th))>0)?0.4:
  439. (S.mode==='close'||S.mode==='open')?0.8:(S.mode==='reg'?0.3:0);
  440. const est=(S.mode==='estop'&&S.estop&&!S.estop.done)?1:0;
  441. /* ---- 油箱 & 泵 ---- */
  442. g.strokeStyle=sub; g.strokeRect(60,455,180,42);
  443. label(g,66,449,'油箱 23 · 400 L');
  444. // 电机+双泵
  445. g.beginPath(); g.arc(110,420,13,0,7); g.stroke(); label(g,104,424,'M',ink,11);
  446. g.beginPath(); g.arc(146,420,13,0,7); g.stroke();
  447. g.fillStyle=ink; g.beginPath(); g.moveTo(146,411); g.lineTo(152,424); g.lineTo(140,424); g.closePath(); g.fill();
  448. label(g,310,464,'25.4 ccm · 1450 rpm ⇒ 36.8 L/min','',9.5);
  449. label(g,310,446,S.power&&S.intState!==1?'泵连续运转':'泵停',S.power&&S.intState!==1?css('--ok'):css('--hp'),10.5);
  450. seg(g,[[146,433],[146,455]],LP,3,0.3); // 吸油
  451. seg(g,[[146,407],[146,352]],HP,4,Qflow||byp?1:0); // 泵出口
  452. /* ---- 25 号卸荷阀 ---- */
  453. seg(g,[[146,352],[240,352]],HP,3.5,byp);
  454. box(g,240,338,44,28,'25');
  455. g.fillStyle=S.v25?css('--hp'):css('--chipbg');
  456. g.fillRect(285,341,10,22);
  457. label(g,240,332,S.v25?'得电=封闭(加载)':'失电=泄回(卸荷)',S.v25?css('--hp'):sub,10);
  458. seg(g,[[284,366],[284,455]].map(p=>[p[0]-22,p[1]]),LP,3.5,byp); // 回油箱
  459. /* 12 单向阀 */
  460. seg(g,[[146,352],[146,318]],HP,4,Qflow);
  461. g.fillStyle=ink; g.beginPath(); g.moveTo(140,318); g.lineTo(152,318); g.lineTo(146,306); g.closePath(); g.fill();
  462. label(g,156,315,'12');
  463. /* 滤芯 11 */
  464. seg(g,[[146,306],[146,282]],HP,4,Qflow);
  465. g.save(); g.translate(146,270); g.rotate(Math.PI/4); g.strokeRect(-9,-9,18,18); g.restore();
  466. label(g,160,272,'11 β25µm');
  467. /* 溢流 10 */
  468. seg(g,[[146,258],[146,196]],HP,4,Qflow);
  469. seg(g,[[146,232],[210,232]],HP,2.5,S.p>=K.pRelief?1:0);
  470. box(g,210,222,26,20,'10'); label(g,240,236,'280 bar',sub);
  471. /* ---- 泵站 70 号手刹蓄能器 10 L(ø7.0 节流 + 20×2.5 管, 12 之后同段) ---- */
  472. const a7x=64,a7y=210,a7w=38,a7h=62, f70=gasFrac(S.p, F.stuck?0:F.p070);
  473. seg(g,[[a7x+a7w/2,a7y+a7h],[a7x+a7w/2,258],[146,258]],HP,3,0);
  474. g.strokeStyle=ink; g.lineWidth=1.6; g.strokeRect(a7x,a7y,a7w,a7h);
  475. const g7=Math.max(2,(a7h-6)*Math.min(1,f70));
  476. g.fillStyle=css('--gas'); g.fillRect(a7x+1.5,a7y+1.5,a7w-3,g7);
  477. g.fillStyle=sub; g.fillRect(a7x+1.5,a7y+1.5+g7,a7w-3,4);
  478. g.fillStyle=css('--hp'); g.globalAlpha=.75;
  479. g.fillRect(a7x+1.5,a7y+5.5+g7,a7w-3,a7h-7-g7); g.globalAlpha=1;
  480. if (F.stuck){ g.strokeStyle=css('--hp'); g.beginPath();
  481. g.moveTo(a7x,a7y); g.lineTo(a7x+a7w,a7y+a7h); g.moveTo(a7x+a7w,a7y); g.lineTo(a7x,a7y+a7h); g.stroke(); }
  482. label(g,36,a7y-8,'70 · 10 L p₀='+(F.stuck?'卡滞':F.p070.toFixed(0)),ink,10);
  483. label(g,36,a7y+a7h+14,'近端主缓冲','',9);
  484. /* 传感器 17 */
  485. g.beginPath(); g.arc(180,196,15,0,7); g.strokeStyle=sub; g.stroke();
  486. const a0=Math.PI*.75+S.p/300*Math.PI*1.5;
  487. g.strokeStyle=ink; g.beginPath(); g.moveTo(180,196);
  488. g.lineTo(180+11*Math.cos(a0),196+11*Math.sin(a0)); g.stroke();
  489. label(g,166,177,'17',sub);
  490. label(g,150,166,S.p.toFixed(0)+' bar',ink,15);
  491. label(g,150,152,'泵站侧 (SCADA 液压压力)','',9.5);
  492. /* ---- P HUB → 旋转接头 → 118 ---- */
  493. seg(g,[[146,196],[146,120],[330,120]],HP,4,Qflow);
  494. g.strokeStyle=sub;
  495. g.beginPath(); g.arc(352,120,16,-1.2,1.2); g.stroke();
  496. g.beginPath(); g.arc(362,120,16,Math.PI-1.2,Math.PI+1.2); g.stroke();
  497. label(g,332,96,'旋转接头');
  498. seg(g,[[378,120],[430,120]],HP,4,Qflow);
  499. g.fillStyle=ink; g.beginPath(); g.moveTo(430,113); g.lineTo(430,127); g.lineTo(444,120); g.closePath(); g.fill();
  500. label(g,412,72,'118 单向',sub);
  501. label(g,390,87,'← 泵→轮毂 单向 · 棘轮之源','',9);
  502. /* ---- 轮毂总管 ---- */
  503. seg(g,[[444,120],[980,120]],HPh,4,Qflow||est?1:0);
  504. /* 传感器107 */
  505. g.beginPath(); g.arc(505,158,15,0,7); g.strokeStyle=sub; g.stroke();
  506. const a1=Math.PI*.75+S.pH/300*Math.PI*1.5;
  507. g.strokeStyle=ink; g.beginPath(); g.moveTo(505,158);
  508. g.lineTo(505+11*Math.cos(a1),158+11*Math.sin(a1)); g.stroke();
  509. seg(g,[[505,143],[505,120]],HPh,2.5,0);
  510. label(g,580,151,'107',sub);
  511. label(g,580,168,S.pH.toFixed(0)+' bar',ink,14);
  512. label(g,580,184,'叶片侧 (桨叶油压)','',9.5);
  513. /* ---- 蓄能器 106(合并示意) ---- */
  514. const ax=590,ay=28,aw=54,ah=86;
  515. seg(g,[[ax+aw/2,ay+ah],[ax+aw/2,120]],HPh,3.5,Qflow*0.7);
  516. g.strokeStyle=ink; g.lineWidth=1.6; g.strokeRect(ax,ay,aw,ah);
  517. const gf=gasFrac(S.mode==='estop'&&F.p0H>=0.5? K.V0H*F.p0H/Math.max(S.Vgas,1e-6):S.pH, F.p0H);
  518. 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));
  519. g.fillStyle=css('--gas'); g.fillRect(ax+1.5,ay+1.5,aw-3,gh);
  520. g.fillStyle=sub; g.fillRect(ax+1.5,ay+1.5+gh,aw-3,5); // 活塞
  521. g.fillStyle=css('--hp'); g.globalAlpha=.75;
  522. g.fillRect(ax+1.5,ay+6.5+gh,aw-3,ah-8-gh); g.globalAlpha=1;
  523. label(g,ax-4,ay-8,'106 · 6×30 L 合并示意 · 顺桨主源 · p₀='+F.p0H.toFixed(0),ink,10);
  524. /* ---- 比例阀 102 → 缸 ---- */
  525. seg(g,[[700,120],[700,206]],HPh,3.5,pitch);
  526. box(g,672,206,56,30,'102');
  527. const pv=S.mode==='estop'?'—':(rateDir()>0?'b 位':rateDir()<0?'a 位':'中位');
  528. label(g,676,226,'比例阀 '+pv,ink,10);
  529. seg(g,[[728,221],[790,221]],HPh,3,pitch);
  530. /* 缸 */
  531. g.strokeStyle=ink; g.strokeRect(790,204,120,34);
  532. const rod=790+18+(S.th/82)*76;
  533. g.fillStyle=sub; g.fillRect(790+2,206,rod-792,30);
  534. g.strokeStyle=ink; g.beginPath(); g.moveTo(rod,221); g.lineTo(rod+56,221); g.stroke();
  535. label(g,792,198,'124/125 · ø125/90×611 ×2缸×3叶',sub,9.5);
  536. /* 叶片角 */
  537. g.save(); g.translate(952,221); g.rotate(-S.th*Math.PI/180);
  538. g.fillStyle=css('--lp'); g.globalAlpha=.85;
  539. g.beginPath(); g.ellipse(0,0,34,9,0,0,7); g.fill(); g.globalAlpha=1; g.restore();
  540. label(g,928,262,'θ = '+S.th.toFixed(1)+'°',ink,14);
  541. /* ---- 紧急旁路 103→121 ---- */
  542. seg(g,[[560,120],[560,300],[770,300],[770,238]],est?HPh:css('--grid'),3, est);
  543. box(g,596,288,40,24,'103'); label(g,600,304,'失电=通',est?ink:sub,9.5);
  544. // 节流孔 121
  545. g.strokeStyle=est?ink:sub; g.beginPath();
  546. 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();
  547. label(g,682,324,'121 ø2.4 顺桨限速',est?css('--hp'):sub,10);
  548. /* ---- 回油 ---- */
  549. seg(g,[[700,236],[700,398],[146,398]],LP,3.5,pitch||est?0.6:0.15);
  550. g.strokeStyle=sub; g.beginPath();
  551. 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();
  552. label(g,628,416,'116/122 ø1.8 回油',sub,9.5);
  553. seg(g,[[146,398],[146,455]],LP,3.5,0.3);
  554. label(g,955,114,'P',sub); label(g,60,392,'T',sub);
  555. /* 模式标注 */
  556. const mtxt={reg:'运行调节:比例阀 102 微调;泵站 70 号=滞环循环的近端主缓冲,轮毂 106 经 118 弱参与',
  557. close:'收桨:102 换向 b 位,需 47 L/min > 泵 36.8 ⇒ 蓄能器补差额',
  558. open:'开桨:102 换向 a 位(116 同时得电)',
  559. estop:'紧急顺桨:全阀失电 → 轮毂 106 经 103→121 推缸(70 号不在此路,其失效不碍顺桨)'};
  560. document.getElementById('circuit-mode-caption').textContent=mtxt[S.mode];
  561. if (S.intState===1) document.getElementById('circuit-mode-caption').textContent+=' · ⚠ 泵间歇失供中…';
  562. g.restore(); dashOff+=1;
  563. }
  564. function rateDir(){ if(S.mode==='close')return 1; if(S.mode==='open')return -1;
  565. const d=S.thCmd-S.th; return Math.abs(d)>.1?Math.sign(d):0; }
  566. /* 活塞特写:双罐并排(左=泵站70号, 右=轮毂106单只) */
  567. function drawPiston(){
  568. const [g,W,H]=fit(pv); g.clearRect(0,0,W,H);
  569. const ink=css('--ink'), sub=css('--sub');
  570. label(g,14,18,'蓄能器剖面 · 泵站70号 vs 轮毂106',ink,12);
  571. const y=44, h=142, w=64;
  572. function tank(x, frac, ttl, cap, stuck){
  573. label(g,x-4,y-8,ttl,ink,10);
  574. g.strokeStyle=ink; g.lineWidth=2; rr(g,x,y,w,h,8); g.stroke();
  575. const gh=Math.max(3,(h-10)*Math.min(1,frac));
  576. g.save(); rr(g,x+2,y+2,w-4,h-4,6); g.clip();
  577. g.fillStyle=css('--gas'); g.fillRect(x,y,w,gh+3);
  578. for(let i=7;i<gh;i+=12){ g.strokeStyle='rgba(0,0,0,.10)'; g.beginPath();
  579. g.moveTo(x+5,y+i); g.lineTo(x+w-5,y+i+3); g.stroke(); }
  580. g.fillStyle=sub; g.fillRect(x,y+gh+1,w,8);
  581. g.fillStyle=css('--hp'); g.globalAlpha=.8; g.fillRect(x,y+gh+9,w,h-gh-9); g.globalAlpha=1;
  582. g.restore();
  583. if (stuck){ g.strokeStyle=css('--hp'); g.lineWidth=2.5;
  584. g.beginPath(); g.moveTo(x+6,y+gh-14); g.lineTo(x+w-6,y+gh+22);
  585. g.moveTo(x+w-6,y+gh-14); g.lineTo(x+6,y+gh+22); g.stroke(); }
  586. label(g,x-4,y+h+13,cap,sub,9);
  587. }
  588. tank(24, gasFrac(S.p, F.stuck?0:F.p070),
  589. '70号 10L/'+(F.stuck?'卡滞':F.p070.toFixed(0)), '泵压主管 · 近端缓冲', F.stuck);
  590. const inEstop=S.mode==='estop'&&F.p0H>=0.5&&S.Vgas>0;
  591. tank(122, inEstop? Math.min(1,S.Vgas/K.V0H) : gasFrac(S.pH,F.p0H),
  592. '106单只 30L/'+F.p0H.toFixed(0), '叶片块×6 · 顺桨主源', false);
  593. label(g,200,y+8,'黄=氮气',css('--gas'),9.5);
  594. label(g,200,y+22,'灰=活塞',sub,9.5);
  595. label(g,200,y+36,'红=油侧',css('--hp'),9.5);
  596. // 吞吐条 + 三态要点
  597. const by=y+h+30, bw=W-40;
  598. label(g,20,by,'本循环吞吐 ΔV '+(S.lastVc!=null?S.lastVc.toFixed(2):'—')+' L(健康 4.9 · 8# 0.50)',ink,11);
  599. g.strokeStyle=css('--grid'); g.strokeRect(20,by+7,bw,12);
  600. const v=Math.min(6,S.lastVc??0);
  601. g.fillStyle=css('--hp'); g.fillRect(20,by+7,bw*v/6,12);
  602. label(g,20,by+36,'70号失氮 → 循环签名剧变(快循环/宽带宽)· 顺桨不受影响',sub,9.5);
  603. label(g,20,by+52,'106失氮 → 循环仅中度变化 · 失电顺桨失去能量源',sub,9.5);
  604. label(g,20,by+68,'双失效 → SCADA 签名与单独70号失效不可分辨 + 顺桨失败',sub,9.5);
  605. label(g,20,by+84,'气体定律 p·V=p₀·V₀(等温)· 气少=硬弹簧 · 无气=钢块',sub,9.5);
  606. }
  607. /* 走线图 */
  608. function chart(cv,rows,ylo,yhi,fmt,refs,title){
  609. const [g,W,H]=fit(cv); g.clearRect(0,0,W,H);
  610. const L=44,R=10,T=22,B=18, ink=css('--ink'), sub=css('--sub');
  611. label(g,L,14,title,ink,11.5);
  612. const win=120, t1=S.t, t0=t1-win;
  613. const X=t=>L+(t-t0)/win*(W-L-R), Y=v=>T+(1-(v-ylo)/(yhi-ylo))*(H-T-B);
  614. g.strokeStyle=css('--grid');
  615. for(let i=0;i<=4;i++){ const v=ylo+(yhi-ylo)*i/4;
  616. g.beginPath(); g.moveTo(L,Y(v)); g.lineTo(W-R,Y(v)); g.stroke();
  617. label(g,4,Y(v)+3,fmt(v),sub,9.5); }
  618. (refs||[]).forEach(r=>{ g.strokeStyle=r[2]; g.setLineDash([5,4]);
  619. g.beginPath(); g.moveTo(L,Y(r[0])); g.lineTo(W-R,Y(r[0])); g.stroke(); g.setLineDash([]);
  620. label(g,W-R-58,Y(r[0])-3,r[1],r[2],9); });
  621. rows.forEach(rw=>{
  622. g.strokeStyle=rw.c; g.lineWidth=rw.w||1.6; g.beginPath(); let pen=false;
  623. for(const b of S.buf){ if(b.t<t0)continue; const v=rw.f(b);
  624. if(v==null){pen=false;continue;}
  625. const x=X(b.t),y=Y(Math.max(ylo,Math.min(yhi,v)));
  626. pen?g.lineTo(x,y):g.moveTo(x,y); pen=true; }
  627. g.stroke();
  628. if(S.buf.length){ const last=rw.f(S.buf[S.buf.length-1]);
  629. 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); }
  630. });
  631. }
  632. /* ================= 判定与 UI ================= */
  633. function showVerdict(){
  634. const e=S.estop, el=document.getElementById('verdict');
  635. if(!e||!e.done) return;
  636. const dt=(e.tEnd-e.t0).toFixed(1);
  637. if(e.ok){ el.className='verdict ok';
  638. const n70=(F.p070<90||F.stuck)? '当前 70 号已注入失效但顺桨仍完成 —— 顺桨主源是轮毂 106(与数据窗 4 次停电全部安全顺桨的实证一致)。'
  639. : '等温理想下界 —— 低预充“勉强完成”不代表实机安全,以现场实测为准。';
  640. 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>`;
  641. } else { el.className='verdict bad';
  642. el.innerHTML=`✗ 顺桨失败:卡滞在 <b class="mono">${S.th.toFixed(0)}°</b>(用时 ${dt} s)· 已放 ${e.dV.toFixed(1)} L · 压力 ${e.pMin.toFixed(0)} bar < 驱动需求 ${K.driveP} bar<br><span style="color:var(--sub);font-size:12px">轮毂 106 失氮 ⇒ 失电顺桨无源。SCADA 充能循环判据对 106 几乎不敏感(循环仅中度变化)——覆盖它的是机组自检容量检查(UP03284 组);两套监测互补,缺一即盲</span>`; }
  643. }
  644. function setMode(m){
  645. S.mode=m;
  646. document.querySelectorAll('#m-reg,#m-close,#m-open,#m-estop')
  647. .forEach(b=>b.classList.remove('on'));
  648. document.getElementById({reg:'m-reg',close:'m-close',open:'m-open',estop:'m-estop'}[m]).classList.add('on');
  649. const el=document.getElementById('verdict');
  650. if(m==='estop'){
  651. S.estop={t0:S.t,done:false,dV:0,pMin:S.pH};
  652. 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;
  653. el.className='verdict'; el.style.display='none';
  654. } else { el.className='verdict'; S.estop=null; }
  655. }
  656. const $=id=>document.getElementById(id);
  657. $('m-reg').onclick=()=>setMode('reg'); $('m-close').onclick=()=>setMode('close');
  658. $('m-open').onclick=()=>setMode('open'); $('m-estop').onclick=()=>setMode('estop');
  659. $('pw').onclick=()=>{ S.power=!S.power; $('pw').textContent=S.power?'有电':'失电';
  660. $('pw').classList.toggle('on',S.power); };
  661. $('spd').oninput=e=>{ $('spdv').textContent=e.target.value+'×'; };
  662. function bind(id,vid,set,fmt){ $(id).oninput=e=>{ set(+e.target.value);
  663. $(vid).textContent=fmt(+e.target.value); }; }
  664. bind('s-p070','v-p070',v=>F.p070=v,v=>v+' bar');
  665. bind('s-p0H','v-p0H',v=>F.p0H=v,v=>v+' bar');
  666. bind('s-leak','v-leak',v=>F.leak=v/10,v=>(v/10).toFixed(1)+'×');
  667. bind('s-pump','v-pump',v=>F.pump=v/100,v=>v+'%');
  668. bind('s-int','v-int',v=>F.inter=v/100,v=>v?(v+'%'):'关');
  669. $('s-stuck').onchange=e=>F.stuck=e.target.checked;
  670. const PRESETS={
  671. healthy: {p070:180,p0H:125,leak:10,pump:100,int:0,stuck:false},
  672. early: {p070:90, p0H:125,leak:10,pump:100,int:0,stuck:false},
  673. collapse:{p070:1, p0H:125,leak:10,pump:100,int:0,stuck:false},
  674. hubfail: {p070:180,p0H:1, leak:10,pump:100,int:0,stuck:false},
  675. bothfail:{p070:1, p0H:1, leak:10,pump:100,int:0,stuck:false},
  676. leak: {p070:180,p0H:125,leak:22,pump:100,int:0,stuck:false},
  677. pumpint: {p070:180,p0H:125,leak:10,pump:100,int:55,stuck:false},
  678. stuck: {p070:180,p0H:125,leak:10,pump:100,int:0,stuck:true},
  679. };
  680. document.querySelectorAll('.presets button').forEach(b=>b.onclick=()=>{
  681. const p=PRESETS[b.dataset.p];
  682. $('s-p070').value=p.p070; F.p070=p.p070; $('v-p070').textContent=p.p070+' bar';
  683. $('s-p0H').value=p.p0H; F.p0H=p.p0H; $('v-p0H').textContent=p.p0H+' bar';
  684. $('s-leak').value=p.leak; F.leak=p.leak/10; $('v-leak').textContent=(p.leak/10).toFixed(1)+'×';
  685. $('s-pump').value=p.pump; F.pump=p.pump/100; $('v-pump').textContent=p.pump+'%';
  686. $('s-int').value=p.int; F.inter=p.int/100; $('v-int').textContent=p.int?(p.int+'%'):'关';
  687. $('s-stuck').checked=p.stuck; F.stuck=p.stuck;
  688. S.cyc.length=0; S.lastVc=null; S.lastPer=null; S.ratchet=S.pH;
  689. });
  690. /* ================= 主循环 ================= */
  691. const cv=$('cv'), pv=$('pv');
  692. let last=performance.now(), carry=0;
  693. /* 表格 DOM 低频差量更新:每帧 innerHTML 重写会把翻译/监控类浏览器扩展的
  694. MutationObserver 打爆导致整页无响应(Chrome+扩展实测)。4 Hz + 无变化不写。 */
  695. let uiAcc=1, mxHtml='', ptHtml='';
  696. function frame(now){
  697. /* rAF 时间戳可早于 performance.now()(macOS26+Chrome151 实测: 首帧 now<last)
  698. → 负 dt 会让 while(n--) 从负数无限递减 = 死循环白屏。负值双重钳制。 */
  699. const dtR=Math.max(0, Math.min(0.06,(now-last)/1000)); last=now;
  700. const speed=+$('spd').value;
  701. carry+=dtR*speed; const DT=0.02;
  702. let n=Math.max(0, Math.min(400,Math.floor(carry/DT))); carry-=n*DT;
  703. if (carry<0) carry=0;
  704. while(n--) step(DT);
  705. try{ drawCircuit(); }catch(e){}
  706. try{ drawPiston(); }catch(e){}
  707. chart($('c1'),[
  708. {n:'叶片侧',c:css('--lp'),w:1.8,f:b=>b.pH},
  709. {n:'泵站侧',c:css('--hp'),w:1.6,f:b=>b.p}],
  710. 100,290,v=>v.toFixed(0),
  711. [[194,'194 加载',css('--ok')],[246,'246 卸荷',css('--hp')]],
  712. '系统压力 bar · 滚动 120 s');
  713. chart($('c2'),[{n:'θ',c:css('--ink'),w:1.8,f:b=>b.th}],
  714. -4,92,v=>v.toFixed(0),[[82,'82° 顺桨',css('--sub')]],'桨距角 °');
  715. chart($('c3'),[{n:'25号阀',c:css('--gas'),w:1.6,f:b=>b.v*0.9+0.05}],
  716. -0.08,1.15,v=>v>0.9?'得电':(v<0.1?'失电':''),[],'25 号卸荷阀状态(得电=加载)');
  717. uiAcc+=dtR;
  718. if (uiAcc>=0.25){ uiAcc=0;
  719. const h1=metrics().map(r=>
  720. `<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('');
  721. if (h1!==mxHtml){ mxHtml=h1; $('mx').innerHTML=h1; }
  722. const h2=pistonRows().map(r=>
  723. `<tr><td style="text-align:left;color:var(--sub)">${r[0]}</td><td><b>${r[1]}</b></td></tr>`).join('');
  724. if (h2!==ptHtml){ ptHtml=h2; $('pistab').innerHTML=h2; }
  725. }
  726. requestAnimationFrame(frame);
  727. }
  728. requestAnimationFrame(frame);
  729. </script>