sc1_sim.html 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868
  1. <meta charset="utf-8">
  2. <title>SC-1 / EN136 4MW 整机联调台</title>
  3. <style>
  4. :root{--bg:#0f1115;--fg:#e6e9ef;--dim:#8b93a3;--line:#232733;--card:#161a22;
  5. --ok:#4ec9a0;--warn:#e0b341;--bad:#e06c6c;--off:#5a6070;--acc:#5aa9e6}
  6. *{box-sizing:border-box}
  7. body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.55 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
  8. header{padding:12px 16px;border-bottom:1px solid var(--line);display:flex;gap:14px;align-items:baseline;flex-wrap:wrap}
  9. h1{font-size:15px;margin:0;font-weight:600}
  10. .sub{color:var(--dim);font-size:12px}
  11. main{display:grid;grid-template-columns:274px 1fr 400px;gap:12px;padding:12px;align-items:start}
  12. @media(max-width:1100px){main{grid-template-columns:1fr}}
  13. .card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:12px}
  14. .card h2{font-size:12px;margin:0 0 8px;color:var(--dim);font-weight:600;letter-spacing:.04em}
  15. .card h3{font-size:12px;margin:10px 0 5px;font-weight:600}
  16. .row{display:flex;justify-content:space-between;gap:8px;padding:2px 0;font-variant-numeric:tabular-nums}
  17. .row b{font-weight:600}
  18. .k{color:var(--dim)}
  19. .v{font-variant-numeric:tabular-nums}
  20. canvas{width:100%;height:auto;display:block;background:#0b0d11;border-radius:6px;border:1px solid var(--line)}
  21. button{background:#1e2430;color:var(--fg);border:1px solid var(--line);border-radius:5px;padding:5px 9px;font-size:12px;cursor:pointer;font-family:inherit}
  22. button:hover{border-color:var(--acc)}
  23. button.on{background:var(--acc);color:#08121c;border-color:var(--acc)}
  24. .btns{display:flex;gap:5px;flex-wrap:wrap;margin:6px 0}
  25. input[type=range]{width:100%}
  26. .badge{display:inline-block;font-size:10px;padding:1px 5px;border-radius:3px;border:1px solid var(--line);color:var(--dim);vertical-align:middle}
  27. .b-tpl{border-color:#4a4030;color:#c8a45a}
  28. .b-meas{border-color:#2e4a3c;color:var(--ok)}
  29. .b-off{border-color:#3a3d47;color:var(--off)}
  30. .b-bad{border-color:#4a2e2e;color:var(--bad)}
  31. .b-ins{border-color:#3a3446;color:#a48ac8}
  32. .mech{border-left:2px solid var(--line);padding-left:9px;margin:9px 0}
  33. .mech.active{border-left-color:var(--ok)}
  34. .mech.disabled{border-left-color:var(--off);opacity:.62}
  35. .mech.dead{border-left-color:var(--bad);opacity:.5}
  36. .mech .t{font-weight:600;font-size:12px}
  37. .mech .d{color:var(--dim);font-size:11px;margin-top:2px}
  38. #soe{height:190px;overflow-y:auto;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.5}
  39. #soe div{border-bottom:1px solid #1a1e27;padding:1px 0}
  40. .t-info{color:var(--dim)} .t-warn{color:var(--warn)} .t-stop{color:var(--bad)} .t-ok{color:var(--ok)}
  41. .note{color:var(--dim);font-size:11px;margin-top:6px;border-top:1px dashed var(--line);padding-top:6px}
  42. .err{background:#2a1a1a;border:1px solid #4a2e2e;color:#f0b8b8;padding:10px;border-radius:6px;margin:12px}
  43. table{width:100%;border-collapse:collapse;font-size:11px;font-variant-numeric:tabular-nums}
  44. td,th{border-bottom:1px solid var(--line);padding:3px 4px;text-align:left}
  45. th{color:var(--dim);font-weight:600}
  46. code{background:#1b2029;padding:1px 4px;border-radius:3px;font-size:11px}
  47. .masked{color:var(--off);letter-spacing:2px}
  48. .chip{display:flex;align-items:baseline;gap:6px;padding:3px 0;border-bottom:1px solid #171b23}
  49. .chip .dot{width:7px;height:7px;border-radius:50%;flex:0 0 7px;background:var(--off)}
  50. .chip.on .dot{background:var(--ok);box-shadow:0 0 6px var(--ok)}
  51. .chip.off .dot{background:#39404e}
  52. .chip.dis .dot{background:#39404e;border:1px solid #4a4f5c}
  53. .chip.dead .dot{background:var(--bad);opacity:.55}
  54. .chip .cn{font-size:11px;font-weight:600;flex:0 0 auto}
  55. .chip .cd{font-size:10px;color:var(--dim);flex:1;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  56. .chip.on .cn{color:var(--ok)} .chip.dead .cn{color:var(--bad);opacity:.75}
  57. #soe{height:150px}
  58. </style>
  59. <header>
  60. <h1>SC-1 / EN136 4MW 整机联调台</h1>
  61. <span class="sub" id="hdr-id">载入中…</span>
  62. <span style="flex:1"></span>
  63. <span class="btns" style="margin:0">
  64. <button id="btn-run">▶ 运行</button>
  65. <button id="btn-reset">重置</button>
  66. <button id="btn-mask">🔓 内部模式(显全部)</button>
  67. </span>
  68. </header>
  69. <div id="boot" class="err" style="display:none"></div>
  70. <main id="app" style="display:none">
  71. <div>
  72. <div class="card">
  73. <h2>风况台</h2>
  74. <div class="row"><span class="k">平均风速</span><span class="v"><b id="v-ws" style="font-size:16px">8.0</b> m/s</span></div>
  75. <input type="range" id="in-ws" min="0" max="28" step="0.5" value="8">
  76. <div class="row"><span class="k">湍流强度</span><span class="v"><b id="v-ti">10</b> %</span></div>
  77. <input type="range" id="in-ti" min="0" max="30" step="1" value="10">
  78. <div class="btns">
  79. <button data-sc="gust">阵风 +8</button>
  80. <button data-sc="ramp">爬到 16 m/s</button>
  81. <button data-sc="lull">跌到 2.5 m/s</button>
  82. </div>
  83. </div>
  84. <div class="card">
  85. <h2>停机程序 · 故障注入</h2>
  86. <div class="btns">
  87. <button data-bp="50">低风停机</button><button data-bp="51">常规停机</button>
  88. <button data-bp="100">快速停机</button><button data-bp="200">过速继电器</button>
  89. <button data-bp="210">急停</button>
  90. </div>
  91. <div class="btns">
  92. <button data-inj="lvrt">电网跌落</button><button data-inj="battery">电池后备</button>
  93. <button data-inj="curtail">限电 50%</button>
  94. </div>
  95. </div>
  96. <div class="card">
  97. <h2>机制状态</h2>
  98. <div id="chips"></div>
  99. </div>
  100. <div class="card">
  101. <h2>SOE 事件序列</h2>
  102. <div id="soe"></div>
  103. </div>
  104. </div>
  105. <div>
  106. <div class="card">
  107. <h2>正视 · 转子与桨距<span class="k" style="font-weight:400"> — 叶片按真实转速转、按真实桨距扭</span></h2>
  108. <canvas id="cv-rotor" width="900" height="470"></canvas>
  109. </div>
  110. <div class="card">
  111. <h2>功率流<span class="k" style="font-weight:400"> — 风 → 气动 → 传动 → 发电机 → 变频 → 电网</span></h2>
  112. <canvas id="cv-flow" width="900" height="130"></canvas>
  113. </div>
  114. </div>
  115. <div>
  116. <div class="card">
  117. <h2>控制律工作点<span class="k" style="font-weight:400"> — 转矩-转速平面</span></h2>
  118. <canvas id="cv-map" width="620" height="420"></canvas>
  119. <div class="note" id="note-map"></div>
  120. </div>
  121. <div class="card">
  122. <h2>运行状态机</h2>
  123. <canvas id="cv-state" width="620" height="96"></canvas>
  124. <div class="row" style="margin-top:4px"><span class="k">当前</span><span class="v" id="v-mode">—</span></div>
  125. </div>
  126. <div class="card">
  127. <h2>证据等级与边界</h2>
  128. <div id="evid"></div>
  129. </div>
  130. </div>
  131. </main>
  132. <script src="./sc1_sim_params.js" onerror="window.__noParamsJs=1"></script>
  133. <script>
  134. 'use strict';
  135. /* ══════════════════════════════════════════════════════════════════
  136. SC-1 / EN136 4MW 整机联调台
  137. ★ 本文件源码内**不含任何 OEM 数值**。所有参数在运行时从同目录 JSON 读取:
  138. control_law_params_rtw.json (RTW 核控层, 379 参数)
  139. param_dict.json (PLC PAR_* 层, 765 参数)
  140. baselines.json (调机期实测基线)
  141. identity.json / operation_modes.json / bp_semantics.json
  142. ⇒ 需经 http 同源提供 (在本目录跑 `python3 -m http.server` 后打开),
  143. file:// 直开会被浏览器 CORS 拦住 —— 这是刻意的, 不是缺陷。
  144. ★ 参数集敏感度: internal · OEM 控制律核心机密。演示/上云一律开"公开模式"。
  145. ══════════════════════════════════════════════════════════════════ */
  146. const F = {}; // 加载后的原始 JSON
  147. let P = {}; // 解析后的数值参数
  148. let MASK = false; // ★内部工程版: 默认全展开(最详细)。掩码按钮供临时截图/外传时用。
  149. // 对外演示走 sc1_sim_demo.html(替代参数, 文件内零 OEM 数值)。
  150. const num = v => { const x = parseFloat(v); return Number.isFinite(x) ? x : null; };
  151. const arr = v => Array.isArray(v) ? v.map(num) : null;
  152. /* ── 载入 ───────────────────────────────────────────── */
  153. async function boot(){
  154. const files = ['control_law_params_rtw.json','param_dict.json','baselines.json',
  155. 'identity.json','operation_modes.json','bp_semantics.json'];
  156. const need = files.map(f=>f.replace('.json',''));
  157. // 路径 A: <script src="./sc1_sim_params.js"> 注入 —— file:// 双击即可用 (script 标签不受 CORS 限制)
  158. if(window.SC1_PARAMS && need.every(k=>window.SC1_PARAMS[k])){
  159. need.forEach(k=>F[k]=window.SC1_PARAMS[k]);
  160. } else
  161. // 路径 B: http 同源 fetch —— 改了 JSON 后无需重新打包, 取最新
  162. try{
  163. const res = await Promise.all(files.map(f => fetch('./'+f).then(r=>{
  164. if(!r.ok) throw new Error(f+' HTTP '+r.status); return r.json();
  165. })));
  166. files.forEach((f,i)=>F[f.replace('.json','')] = res[i]);
  167. }catch(e){
  168. document.getElementById('boot').style.display='block';
  169. document.getElementById('boot').innerHTML =
  170. '<b>参数未载入</b> — '+e.message+
  171. '<br><br>本页刻意不把 OEM 数值内嵌进 HTML(参数集标 <code>internal · OEM 控制律核心机密</code>),'+
  172. '改从<b>同目录的独立文件</b>取。两条路任一即可:'+
  173. '<br>① <b>直接双击打开</b>:确保同目录有 <code>sc1_sim_params.js</code>'+
  174. '(没有就跑 <code>python3 scripts/build_sc1_sim_params.py</code> 生成)'+
  175. '<br>② <b>起个本地服务</b>:<code>cd resources/oem_envision_sc1_rudong2014 &amp;&amp; python3 -m http.server 8791</code>'+
  176. ' 然后开 <code>http://localhost:8791/sc1_sim.html</code>';
  177. return;
  178. }
  179. parseParams();
  180. buildStatic();
  181. document.getElementById('app').style.display='grid';
  182. reset(); draw();
  183. }
  184. function parseParams(){
  185. const R = F.control_law_params_rtw, D = F.param_dict.params;
  186. const g = k => num(R[k]);
  187. const gp = k => num((D[k]||{}).golden_kk_2014_12 ?? (D[k]||{}).default);
  188. P = {
  189. // 转速链
  190. ratedRpm:g('GenSpd_RatedLoadSpeed'), minLoadCage:g('GenSpd_MinLoadSpeed_Cage'),
  191. overSpd:g('GenSpd_OverSpeed'), constSpd:g('GenSpd_ConstantSpeed'), speedUp:g('GenSpd_Speedup'),
  192. // 转矩
  193. tqMax:g('Con_MaxTorque'), tqMin:g('Con_MinTorque'), tqRamp:g('Con_TorqueMaxRampRate'),
  194. optGainX:arr(R['CON_GENSPEED_OPTGAIN_X']), optGainY:arr(R['CON_GENSPEED_OPTGAIN_Y']),
  195. optimalGain:g('OptimalGain'),
  196. // 额定以上变桨 PI 增益调度 (按桨角线性)
  197. kpSlope:g('Con_PitchAboveRated_KpSlope'), kpInt:g('Con_PitchAboveRated_KpInterCept'),
  198. tiSlope:g('Con_PitchAboveRated_TiSlope'), tiInt:g('Con_PitchAboveRated_TiInterCept'),
  199. pitLo:g('Con_PitchAboveRated_LowerPitchAngle'), pitHi:g('Con_PitchAboveRated_UpperPitchAngle'),
  200. // 变桨速率族
  201. rSlow:g('Con_PitchSlowRate'), rSlowDur:g('Con_PitchSlowRateDuration'),
  202. rStop:g('Con_PitchNormalStopRate'), rFast:g('Con_PitchFastRate'), rFastDur:g('Con_PitchFastRateDuration'),
  203. rMax:g('Con_PitchMaxRate'), rBatt:g('Con_PitchBatteryMaxRate'),
  204. // 塔共振禁区: PLC 侧生效 / RTW 侧 TEZ
  205. tezLo:g('TEZLowerRpm'), tezMid:g('TEZRpm'), tezHi:g('TEZUpperRpm'),
  206. tezHyst:g('TEZ_Hyst'), tezEnable:g('TEZ_Enable'),
  207. resoUp:gp('PAR_rTowerResoGenSpeedUp'), resoDn:gp('PAR_rTowerResoGenSpeedDown'),
  208. // 塔阻尼
  209. fTower:g('TowerFrequency'), twrG:g('CON_PITCH_TWR_G'),
  210. ssEnable:g('TwrSideSideTorq_Enable'), ssKp:g('TwrSideSideTorq_Kp'),
  211. faOn:g('TwrFbkForeAftPowerPctOn'), faOff:g('TwrFbForeAftkPowerPctOff'),
  212. // 传动链
  213. dtF1:g('Con_DRVDamper_Freq'), dtF2:g('Con_DRVDamper_Freq2'),
  214. dtK1:g('Con_DRVDamper_K'), dtK2:g('Con_DRVDamper_K2'),
  215. notch:g('Con_NotchFilter_Freq'), dtFreq:g('DTFrequency'),
  216. // 其他
  217. lvrtClamp:g('LVRT_TorqLimitClampCoeff'), ts:g('Ts'),
  218. bladeEdge:g('EnableBladeEdgeMonitor'), cageDfig:g('Cage_DFIG_Enable'),
  219. };
  220. const I = F.identity.identity;
  221. P.ratio = I.drivetrain.ratio_gen_rotor;
  222. const mkw = String(I.generator.rating||'').match(/(\d+(?:\.\d+)?)\s*kW/i);
  223. P.ratedKW = mkw ? parseFloat(mkw[1]) : null; // 从 identity.generator.rating 解析, 不硬编码
  224. P.torsFn = I.drivetrain.torsional_fn_hz;
  225. const BLm = F.baselines.baselines.shutdown_programs_measured || {};
  226. P.feather = (BLm.BP100 && BLm.BP100.feather_pos_deg) ?? null; // 顺桨到位角, 取自实测基线
  227. P.hwLimit = (I.pitch.hw_limits_deg||[])[0] ?? null; // 硬限位, 取自 identity
  228. }
  229. /* ── 掩码 ───────────────────────────────────────────── */
  230. function fmt(v, d=2, unit=''){
  231. if(v===null||v===undefined) return '<span class="masked">—</span>';
  232. if(MASK) return '<span class="masked">▪▪▪</span>'+(unit?' '+unit:'');
  233. return (typeof v==='number' ? (Math.abs(v)>=1000? v.toFixed(0) : v.toFixed(d)) : v)+(unit?' '+unit:'');
  234. }
  235. /* 掩码不止数值 —— 参数标识名同样泄露控制架构; 场名/业主属客户信息。
  236. 露出的只保留: 机制名称、功能有无、行为形态。 */
  237. function pn(name){ return MASK ? '<span class="masked">▪▪▪▪</span>' : '<code>'+name+'</code>'; }
  238. function txt(s){ return MASK ? '<span class="masked">▪▪▪</span>' : s; }
  239. /* 文本节点级掩码: 只改文字, 不碰标签与属性 —— 用于正文段落里散落的分析数字。
  240. 保留句子结构与结论方向, 只把数字与参数标识顶掉。 */
  241. function maskTextNodes(root){
  242. if(!MASK || !root) return;
  243. const w=document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
  244. const hit=[]; let n;
  245. while(n=w.nextNode()) hit.push(n);
  246. hit.forEach(n=>{
  247. n.nodeValue = n.nodeValue
  248. .replace(/-?\d+(?:[.,]\d+)*\s*(?:°\/s|℃|Hz|rpm|kW|kNm|%|°|s\b)?/g, m=>/^\s*$/.test(m)?m:'▪▪')
  249. .replace(/[A-Z][A-Za-z0-9_]{6,}/g, '▪▪▪▪');
  250. });
  251. root.querySelectorAll('code').forEach(c=>{ c.textContent='▪▪▪▪'; });
  252. }
  253. /* 正文里的散落数字: 掩码时整段用 ▪ 顶掉数字, 保留句子结构 */
  254. function pz(s){ return MASK ? s.replace(/-?\d+(?:\.\d+)?%?/g,'▪▪') : s; }
  255. /* ══════════════ 仿真核心 ══════════════ */
  256. const DT = 0.05;
  257. const WS_START = 3.5, WS_STOP = 3.0; // 起停风门 = 仿真常数, 非 OEM 参数
  258. let S, running=false, tPrev=0;
  259. function reset(){
  260. S = { t:0, ws:parseFloat(document.getElementById('in-ws').value), wsInst:8,
  261. rpm:0, tq:0, pitch:90, pwr:0, mode:7, bp:null, bpT:0, bpPhase:0,
  262. tezTimer:0, tezDir:0, lvrt:0, batt:false, curtail:0,
  263. faActive:false, integ:0, hist:[] };
  264. document.getElementById('soe').innerHTML='';
  265. soe('info','联调台重置 · 状态 7 CON_iStandby');
  266. }
  267. function soe(cls, msg){
  268. const el=document.getElementById('soe');
  269. const d=document.createElement('div');
  270. d.className='t-'+cls;
  271. d.textContent='['+S.t.toFixed(1).padStart(6)+'s] '+msg;
  272. el.insertBefore(d, el.firstChild);
  273. while(el.children.length>150) el.removeChild(el.lastChild);
  274. }
  275. // 转矩律: 额定以下最优增益查表 (RTW CON_GENSPEED_OPTGAIN_X/Y), 额定以上恒功率
  276. function torqueLaw(rpm){
  277. if(!P.optGainX||!P.optGainY) return 0;
  278. const X=P.optGainX, Y=P.optGainY;
  279. if(rpm<=X[0]) return P.optimalGain*rpm*rpm*1e-3;
  280. const constPwr = Math.min(P.tqMax, P.ratedKW*1000/(Math.max(rpm,1)*Math.PI/30))/1000;
  281. if(rpm>=P.ratedRpm) return constPwr;
  282. let k=Y[Y.length-1];
  283. for(let i=0;i<X.length-1;i++) if(rpm>=X[i]&&rpm<X[i+1]){
  284. const f=(rpm-X[i])/(X[i+1]-X[i]); k=Y[i]+f*(Y[i+1]-Y[i]); break;
  285. }
  286. const optTq = P.optimalGain*k*rpm*rpm*1e-3;
  287. // 额定前 60 rpm 内线性过渡到恒功率支 —— 否则转速在额定点上下抖时转矩指令在两支间跳变
  288. // (首版实测: 转矩曲线高频抖振; 这是控制律接缝, 非湍流)
  289. const band = 60;
  290. if(rpm > P.ratedRpm-band){
  291. const f=(rpm-(P.ratedRpm-band))/band;
  292. return optTq*(1-f) + constPwr*f;
  293. }
  294. return optTq;
  295. }
  296. // 塔共振禁区: PLC TowerReso 带生效; RTW TEZ 按 TEZ_Enable
  297. function tezGate(rpm){
  298. const lo=P.resoDn, hi=P.resoUp;
  299. if(lo===null||hi===null) return {inZone:false};
  300. const inZone = rpm>lo && rpm<hi;
  301. return {inZone, lo, hi};
  302. }
  303. // 塔架 fore-aft 阻尼: 走变桨通道, 按功率百分比滞环投入/退出
  304. function faDamper(pwrPct){
  305. if(P.faOn===null) return false;
  306. if(!S.faActive && pwrPct>P.faOn) { S.faActive=true; soe('ok','塔架 fore-aft 阻尼 投入 (功率 > 投入门)'); }
  307. if( S.faActive && pwrPct<P.faOff){ S.faActive=false; soe('info','塔架 fore-aft 阻尼 退出 (功率 < 退出门)'); }
  308. return S.faActive;
  309. }
  310. // 停机程序: 按 baselines.shutdown_programs_measured 的逐段实测速率
  311. function bpPitchRate(){
  312. const M = F.baselines.baselines.shutdown_programs_measured || {};
  313. const k = 'BP'+S.bp, m = M[k];
  314. if(S.bp===200 || S.bp===210){
  315. // 安全链路径: 用 BP200 的逐段实测 (baselines.shutdown_programs_measured.BP200)
  316. const m200 = M['BP200'];
  317. if(m200 && m200.pitch_phases_dps) return m200.pitch_phases_dps[0];
  318. return P.rMax; // 无实测则退回参数限幅, 不硬编码
  319. }
  320. if(!m || !m.pitch_phases_dps) return P.rStop ?? 2;
  321. const ph = m.pitch_phases_dps, win = m.pitch_phase_windows_s;
  322. if(win){
  323. for(let i=0;i<win.length;i++){
  324. const [a,b]=String(win[i]).split('-').map(Number);
  325. if(S.bpT>=a && S.bpT<b) { S.bpPhase=i; return ph[i]; }
  326. }
  327. S.bpPhase=ph.length-1; return ph[ph.length-1];
  328. }
  329. // BP51: 卸载期 → 卸载后。卸载完成以转矩跌破触发时刻的 5% 判 (与 K2 录波同口径)
  330. if(S.bpTq0===undefined || S.bpT<=DT) S.bpTq0 = Math.max(S.tq, 1e-6);
  331. const unloaded = S.tq < 0.05*S.bpTq0;
  332. S.bpPhase = unloaded?1:0;
  333. return unloaded ? ph[1] : ph[0];
  334. }
  335. function step(){
  336. // 风
  337. const ti=parseFloat(document.getElementById('in-ti').value)/100;
  338. S.ws = parseFloat(document.getElementById('in-ws').value);
  339. S.wsInst += ((S.ws-S.wsInst)*0.02) + (Math.random()-0.5)*S.ws*ti*0.25;
  340. S.wsInst = Math.max(0,S.wsInst);
  341. if(S.gust>0){ S.wsInst += S.gust; S.gust*=0.97; if(S.gust<0.05) S.gust=0; }
  342. const M = F.operation_modes.modes;
  343. /* 状态机 */
  344. if(S.bp!==null){ // 停机程序执行中
  345. S.bpT += DT;
  346. const r = bpPitchRate();
  347. S.pitch = Math.min(P.feather ?? P.hwLimit, S.pitch + r*DT);
  348. if(S.bp>=100) S.tq = Math.max(0, S.tq - (P.tqMax||3e4)/1000*DT/0.4); // 转矩切断 0.4s
  349. else S.tq = Math.max(0, S.tq - (P.tqMax||3e4)/1000*DT/16.8); // 速度环卸载 ~16.8s
  350. if(P.feather && S.pitch>=P.feather-0.1 && S.rpm<10){
  351. soe('ok','停机程序完成 · 顺桨到位 '+(MASK?'▪▪':S.bpT.toFixed(1))+'s · 转速归零'); S.bp=null; S.mode=7; S.bpTq0=undefined;
  352. } else if(S.bpT>600){
  353. soe('warn','停机程序超 600s 未完成 · 顺桨 '+S.pitch.toFixed(1)+'° 转速 '+S.rpm.toFixed(0)+' rpm — 如实报, 不假装完成');
  354. S.bp=null; S.bpTq0=undefined;
  355. }
  356. } else {
  357. // 常规运行链
  358. if(S.mode===7 && S.wsInst>WS_START){ S.mode=13; soe('info','13 CON_iStartMode · 开桨'); }
  359. if(S.mode===13){
  360. const openRate = F.baselines.baselines.startup_seq_n2.mode13_open_rate_dps || -2.0;
  361. S.pitch = Math.max(0, S.pitch + openRate*DT);
  362. const cut = (F.baselines.baselines.startup_seq_n2.cutin_gen_rpm||[820,840])[0];
  363. if(S.rpm>cut){ S.mode=19; soe('ok','19 CON_iCAGE_GutIn · 全功率软切入'); S.modeT=0; }
  364. }
  365. if(S.mode===19){ S.modeT=(S.modeT||0)+DT; if(S.modeT>0.2){ S.mode=20; soe('ok','20 CON_iCAGE_GridOp · 并网'); } }
  366. if(S.mode===7 && S.wsInst<WS_STOP) S.pitch = Math.min(90, S.pitch+1.0*DT);
  367. }
  368. /* 气动 → 转速
  369. ⚠ 以下 Cp(λ,β) 经验式、转子半径、折算惯量 J 均为**仿真常数, 非 OEM 参数**, 不可外引。
  370. 用 Cq=Cp/λ 算转矩而非 P/ω: 后者在 ω→0 时发散, 会让机组永远自启动不了(首版实测卡在 95 rpm)。 */
  371. const R = 68; // EN136 → 转子半径 (型号代号推得, 非包内参数)
  372. const rho = 1.225, A = Math.PI*R*R, J = 620; // J = 折算到高速轴的等效惯量 (仿真常数)
  373. const wr = (S.rpm/P.ratio)*Math.PI/30; // 转子角速度 rad/s
  374. const lam = wr*R/Math.max(S.wsInst,0.5);
  375. const b1 = S.pitch;
  376. const li = 1/( 1/(lam+0.08*b1+1e-6) - 0.035/(b1*b1*b1+1) );
  377. let cp = 0.5176*(116/li - 0.4*b1 - 5)*Math.exp(-21/li) + 0.0068*lam;
  378. cp = Math.max(0, Math.min(0.48, cp));
  379. const cq = lam>0.15 ? cp/lam : cp/0.15; // 转矩系数, λ→0 不发散
  380. const Qrot = 0.5*rho*A*R*S.wsInst*S.wsInst*cq/1000; // kNm (转子侧)
  381. const Taero = Qrot/P.ratio; // kNm (折算到高速轴)
  382. const Paero = 0.5*rho*A*Math.pow(S.wsInst,3)*cp/1000; // kW
  383. /* 转矩指令 */
  384. if(S.bp===null && S.mode===20){
  385. let cmd = torqueLaw(S.rpm);
  386. // 额定以上: 变桨 PI 按桨角线性增益调度
  387. if(S.rpm>P.ratedRpm || S.pitch>0.2){
  388. const kp = (P.kpInt??0) + (P.kpSlope??0)*Math.min(Math.max(S.pitch,P.pitLo??0),P.pitHi??24);
  389. const ti = (P.tiInt??10) + (P.tiSlope??0)*S.pitch;
  390. const err = S.rpm - P.ratedRpm;
  391. /* 抗饱和 (实跑逮到的 bug): 并网发生在远低于额定的转速上, err 长期为大负值 ⇒
  392. 积分器卷到 -248 并锁死, 之后在额定附近 ±50 rpm 摆, 最终被湍流推过超速阈跳 BP200。
  393. 首版"稳态 1546-1551"是观察窗不够长, 不是通过 —— 窗口短会把发散读成稳定。
  394. 两道: ①桨距贴在 0° 下限且误差仍为负 ⇒ 停止继续下卷 ②积分项硬限幅。 */
  395. const railed = (S.pitch <= 0.001 && err < 0);
  396. if(!railed) S.integ += err*DT/Math.max(ti,0.1);
  397. const IMAX = 200;
  398. S.integ = Math.max(-IMAX, Math.min(IMAX, S.integ));
  399. /* ⚠ KSCALE 是**仿真标度, 非 OEM 量**: RTW 的 Kp/Ti 单位未锚(与 Con_DRVDamper_Freq
  400. 等频率类同属"定标未锚"一族), 无法把它们换算成 °/s per rpm。这里用一个拟合标度让
  401. 闭环行为合理, 因此**本页的增益绝对值不可外引**, 只有"Kp 随桨角线性增大"这个**形状**
  402. 来自参数集。KSCALE 由 12 m/s 长窗压力测试定(该点是额定过渡, 最难)。 */
  403. const KSCALE = 0.0032;
  404. let pitCmd = (kp*err + S.integ)*KSCALE + (S.rpm-(S.rpmPrev??S.rpm))/DT*0.02;
  405. S.rpmPrev = S.rpm;
  406. if(faDamper(S.pwr/P.ratedKW)) pitCmd += Math.sin(2*Math.PI*(P.fTower||0.315)*S.t)*(P.twrG||0)*0.02;
  407. const lim = (S.batt ? P.rBatt : P.rMax) ?? 0; // 无参数则不动桨, 不编造限幅
  408. S.pitch = Math.max(0, Math.min(P.feather ?? P.hwLimit, S.pitch + Math.max(-lim,Math.min(lim,pitCmd))*DT));
  409. } else { S.integ=0; }
  410. if(S.curtail>0) cmd = Math.min(cmd, cmd*(1-S.curtail));
  411. if(S.lvrt>0){ cmd *= (P.lvrtClamp??0.2); S.lvrt-=DT; if(S.lvrt<=0) soe('ok','LVRT 结束 · 转矩限幅解除'); }
  412. S.tq += Math.max(-(P.tqRamp||500)*DT, Math.min((P.tqRamp||500)*DT, cmd-S.tq));
  413. }
  414. /* 阻力项 (仿真常数, 非 OEM): 轴承摩擦 + 风阻 + 顺桨叶片气动阻力。
  415. 缺了它顺桨到位后转速会永远挂住 —— 首版实测停机程序跑不完(完成=false)。 */
  416. const wg = S.rpm*Math.PI/30;
  417. const featherDrag = S.pitch>60 ? 0.02*(S.pitch-60)/30 : 0; // 顺桨段附加阻力
  418. const Qdrag = (0.004 + featherDrag)*wg + 2e-5*wg*wg; // kNm, 恒为制动方向
  419. /* 转速积分: dω/dt = (Q_aero − Q_em − Q_drag)/J , ω[rad/s] → rpm */
  420. const dw = (Taero - S.tq - Qdrag)*1000/J; // rad/s²
  421. S.rpm = Math.max(0, S.rpm + dw*DT*30/Math.PI);
  422. S.pwr = Math.max(0, S.tq*1000*(S.rpm*Math.PI/30)/1000);
  423. /* 塔共振禁区 dwell 计时 */
  424. const tz = tezGate(S.rpm);
  425. if(tz.inZone){
  426. S.tezTimer += DT;
  427. const maxT = num((F.param_dict.params['PAR_tResonanceModeTimeMax']||{}).golden_kk_2014_12)/1000;
  428. if(maxT && S.tezTimer>maxT && !S.tezWarned){
  429. soe('warn','共振区停留超时 · PAR_tResonanceModeTimeMax'); S.tezWarned=true;
  430. }
  431. } else { S.tezTimer=0; S.tezWarned=false; }
  432. /* 超速保护 */
  433. if(P.overSpd && S.rpm>P.overSpd && S.bp===null){
  434. soe('stop','超速 · 触发 BP200 (安全链 过速继电器)'); trigBP(200);
  435. }
  436. S.t += DT;
  437. if(S.hist.length===0 || S.t-S.hist[S.hist.length-1].t > 0.2)
  438. S.hist.push({t:S.t, rpm:S.rpm, tq:S.tq, pitch:S.pitch, pwr:S.pwr});
  439. if(S.hist.length>900) S.hist.shift();
  440. }
  441. function trigBP(n){
  442. S.bp=n; S.bpT=0; S.bpPhase=0;
  443. const lad = (F.bp_semantics.ladder||[]).find(x=>x.bp===n);
  444. const inCode = [0,50,51,100,198,200,210].includes(n);
  445. soe('stop',(MASK?'停机程序':'BP'+n)+' 触发 · 代码 POU '+(inCode?'有':'无')+' / 交付阶梯 '+(lad?'有':'无')
  446. +(inCode&&!lad?' ← 三套编号并存的实例, 非错误':''));
  447. }
  448. /* ══════════════ 绘制 ══════════════ */
  449. function draw(){ drawRotor(); drawFlow(); drawMap(); drawState(); updateReadouts(); updateChips(); }
  450. function stateColor(){
  451. if(S.bp!==null) return '#e06c6c';
  452. if(S.mode===20) return '#4ec9a0';
  453. if(S.mode===13||S.mode===19) return '#e0b341';
  454. return '#6b7near';
  455. }
  456. function stCol(){ return S.bp!==null?'#e06c6c' : S.mode===20?'#4ec9a0' : (S.mode===13||S.mode===19)?'#e0b341':'#6b7280'; }
  457. /* ── 正视风机: 塔+机舱+三桨, 按真实转速转、按真实桨距扭 ── */
  458. function drawRotor(){
  459. const c=document.getElementById('cv-rotor'), x=c.getContext('2d');
  460. const W=c.width,H=c.height; x.clearRect(0,0,W,H);
  461. const g=x.createLinearGradient(0,0,0,H); g.addColorStop(0,'#0c1018'); g.addColorStop(1,'#0a0d13');
  462. x.fillStyle=g; x.fillRect(0,0,W,H);
  463. const hubX=W*0.56, hubY=H*0.40, Rb=H*0.33; // 叶片长度(像素)
  464. const col=stCol();
  465. /* 风矢: 左侧箭头, 长度 ∝ 风速 */
  466. const wsN=Math.min(S.wsInst/25,1);
  467. x.strokeStyle='#3d5a80'; x.fillStyle='#3d5a80'; x.lineWidth=1.5;
  468. for(let k=0;k<6;k++){
  469. const yy=H*0.12+k*H*0.13, L=30+wsN*120;
  470. x.beginPath(); x.moveTo(20,yy); x.lineTo(20+L,yy); x.stroke();
  471. x.beginPath(); x.moveTo(20+L,yy); x.lineTo(20+L-7,yy-3.5); x.lineTo(20+L-7,yy+3.5); x.closePath(); x.fill();
  472. }
  473. x.fillStyle='#8b93a3'; x.font='12px sans-serif';
  474. x.fillText('风 '+(MASK?'▪▪':S.wsInst.toFixed(1))+' m/s', 20, H*0.06);
  475. /* 塔筒 (锥形) */
  476. const base=H*0.98, tw0=26, tw1=13;
  477. x.fillStyle='#252b36';
  478. x.beginPath(); x.moveTo(hubX-tw0/2,base); x.lineTo(hubX-tw1/2,hubY+14);
  479. x.lineTo(hubX+tw1/2,hubY+14); x.lineTo(hubX+tw0/2,base); x.closePath(); x.fill();
  480. /* 机舱 */
  481. x.fillStyle='#2e3542'; x.fillRect(hubX-16,hubY-15,58,30);
  482. x.fillStyle='#1c212b'; x.fillRect(hubX+30,hubY-9,14,18);
  483. /* 三桨 */
  484. S.azi = (S.azi||0) + (S.rpm/P.ratio)*Math.PI/30*DT*4;
  485. for(let b=0;b<3;b++){
  486. const a=S.azi + b*2*Math.PI/3;
  487. x.save(); x.translate(hubX,hubY); x.rotate(a);
  488. // 叶片平面形: 根部窄→中段宽→梢部收
  489. x.fillStyle=col; x.globalAlpha=.88;
  490. x.beginPath();
  491. x.moveTo(-6,-8); x.quadraticCurveTo(-13,-Rb*0.45,-7,-Rb*0.92);
  492. x.lineTo(-1,-Rb); x.lineTo(4,-Rb*0.92);
  493. x.quadraticCurveTo(11,-Rb*0.45,7,-8); x.closePath(); x.fill();
  494. x.globalAlpha=1;
  495. // 70% 展向的翼型截面: 按桨距角扭转 —— 这是"桨距"看得见的地方
  496. const rr=-Rb*0.70, ch=26, be=S.pitch*Math.PI/180;
  497. x.save(); x.translate(0,rr); x.rotate(be);
  498. x.strokeStyle='#0b0d11'; x.lineWidth=3;
  499. x.beginPath(); x.ellipse(0,0,ch/2,4.5,0,0,2*Math.PI); x.stroke();
  500. x.fillStyle='#e6e9ef'; x.beginPath(); x.ellipse(0,0,ch/2,4.5,0,0,2*Math.PI); x.fill();
  501. x.strokeStyle=col; x.lineWidth=2;
  502. x.beginPath(); x.moveTo(-ch/2,0); x.lineTo(ch/2,0); x.stroke();
  503. x.restore();
  504. x.restore();
  505. }
  506. /* 轮毂 */
  507. x.fillStyle='#3a4250'; x.beginPath(); x.arc(hubX,hubY,13,0,2*Math.PI); x.fill();
  508. x.fillStyle=col; x.beginPath(); x.arc(hubX,hubY,6,0,2*Math.PI); x.fill();
  509. /* 桨距放大示意: 右下角单叶截面 + 角度弧 */
  510. const px=W-118, py=H-92;
  511. x.strokeStyle='#2a3040'; x.beginPath(); x.moveTo(px-52,py); x.lineTo(px+52,py); x.stroke();
  512. x.fillStyle='#5a6070'; x.font='10px sans-serif'; x.fillText('旋转平面', px-52, py+13);
  513. x.save(); x.translate(px,py); x.rotate(S.pitch*Math.PI/180);
  514. x.strokeStyle=col; x.lineWidth=3.5; x.beginPath(); x.moveTo(-46,0); x.lineTo(46,0); x.stroke();
  515. x.fillStyle=col; x.beginPath(); x.ellipse(0,0,46,7,0,0,2*Math.PI); x.globalAlpha=.28; x.fill(); x.globalAlpha=1;
  516. x.restore();
  517. x.strokeStyle=col; x.lineWidth=1.4; x.beginPath();
  518. x.arc(px,py,30,0,S.pitch*Math.PI/180); x.stroke();
  519. x.fillStyle=col; x.font='600 15px sans-serif';
  520. x.fillText('β = '+(MASK?'▪▪':S.pitch.toFixed(1))+'°', px-34, py-42);
  521. /* 大字读数 */
  522. const mk=v=>MASK?'▪▪▪':v;
  523. const rd=[['转速',mk(S.rpm.toFixed(0)),'rpm',S.rpm>(P.overSpd||1e9)*0.97?'#e06c6c':'#e6e9ef'],
  524. ['转矩',mk(S.tq.toFixed(1)),'kNm','#e0b341'],
  525. ['功率',mk(S.pwr.toFixed(0)),'kW','#4ec9a0'],
  526. ['转子',mk((S.rpm/P.ratio).toFixed(2)),'rpm','#8b93a3']];
  527. rd.forEach((r,k)=>{
  528. const bx=20, by=H*0.44+k*30;
  529. x.fillStyle='#8b93a3'; x.font='11px sans-serif'; x.fillText(r[0], bx, by);
  530. x.fillStyle=r[3]; x.font='600 19px ui-monospace,Menlo,monospace'; x.fillText(r[1], bx+34, by+1);
  531. x.fillStyle='#5a6070'; x.font='10px sans-serif'; x.fillText(r[2], bx+34+x.measureText(r[1]).width*1.55, by);
  532. });
  533. /* 状态角标 */
  534. x.fillStyle=col; x.font='600 13px sans-serif';
  535. const lab = S.bp!==null? ('停机程序'+(MASK?'':' BP'+S.bp)+' · 段'+(S.bpPhase+1))
  536. : S.mode===20? '并网发电' : S.mode===13? '启动 · 开桨' : S.mode===19? '软切入' : '待机';
  537. x.fillText(lab, W-x.measureText(lab).width-16, 22);
  538. }
  539. /* ── 功率流链条 ── */
  540. function drawFlow(){
  541. const c=document.getElementById('cv-flow'), x=c.getContext('2d');
  542. const W=c.width,H=c.height; x.fillStyle='#0b0d11'; x.fillRect(0,0,W,H);
  543. const Pn = P.ratedKW || Math.max(1, S.pwr); // 无额定值则按当前功率归一, 不写死铭牌值
  544. const pa = S.pwr/0.94/0.97, stages=[
  545. {n:'风', v:null, c:'#3d5a80'},
  546. {n:'气动', v:Math.max(0,pa), c:'#5aa9e6'},
  547. {n:'齿箱', v:Math.max(0,S.pwr/0.94), c:'#7bb8c9'},
  548. {n:'发电机', v:Math.max(0,S.pwr/0.97), c:'#e0b341'},
  549. {n:'变频器', v:Math.max(0,S.pwr), c:'#c99a4e'},
  550. {n:'电网', v:Math.max(0,S.pwr*(1-(S.curtail||0))), c:'#4ec9a0'},
  551. ];
  552. const bw=W/stages.length, maxH=H-46;
  553. stages.forEach((s,i)=>{
  554. const cx=i*bw+bw/2;
  555. if(s.v!==null){
  556. const h=Math.max(3, Math.min(1,s.v/Pn)*maxH);
  557. x.fillStyle=s.c; x.globalAlpha=.85;
  558. x.fillRect(cx-bw*0.3, H-24-h, bw*0.6, h); x.globalAlpha=1;
  559. x.fillStyle='#e6e9ef'; x.font='600 12px ui-monospace,monospace';
  560. const lb=s.v.toFixed(0);
  561. const lb2=MASK?'▪▪▪':lb; x.fillText(lb2, cx-x.measureText(lb2).width/2, H-30-h);
  562. } else {
  563. x.fillStyle=s.c; x.font='600 13px sans-serif';
  564. const lb=(MASK?'▪▪':S.wsInst.toFixed(1))+' m/s';
  565. x.fillText(lb, cx-x.measureText(lb).width/2, H-24-maxH*0.5);
  566. }
  567. x.fillStyle='#8b93a3'; x.font='11px sans-serif';
  568. x.fillText(s.n, cx-x.measureText(s.n).width/2, H-8);
  569. if(i<stages.length-1){
  570. x.strokeStyle='#2a3040'; x.beginPath();
  571. x.moveTo(cx+bw*0.32,H-24-maxH*0.25); x.lineTo(cx+bw*0.68,H-24-maxH*0.25); x.stroke();
  572. }
  573. });
  574. x.strokeStyle='#232733'; x.beginPath(); x.moveTo(0,H-24); x.lineTo(W,H-24); x.stroke();
  575. x.fillStyle='#5a6070'; x.font='10px sans-serif'; x.fillText('kW · 柱高对额定归一 (效率取仿真常数)', 6, 13);
  576. }
  577. /* ── 转矩-转速 工作点图: 把控制律画出来 ── */
  578. function drawMap(){
  579. const c=document.getElementById('cv-map'), x=c.getContext('2d');
  580. const W=c.width,H=c.height; x.fillStyle='#0b0d11'; x.fillRect(0,0,W,H);
  581. if(!P.overSpd||!P.tqMax) return;
  582. const L=52,B=34, rMax=P.overSpd*1.03, tMax=P.tqMax/1000*1.15;
  583. const sx=r=>L+r/rMax*(W-L-14), sy=q=>H-B-q/tMax*(H-B-16);
  584. // 网格
  585. x.strokeStyle='#171b23'; x.lineWidth=1;
  586. for(let i=0;i<=5;i++){ const yy=H-B-(H-B-16)*i/5; x.beginPath(); x.moveTo(L,yy); x.lineTo(W-14,yy); x.stroke(); }
  587. for(let i=0;i<=4;i++){ const xx=L+(W-L-14)*i/4; x.beginPath(); x.moveTo(xx,16); x.lineTo(xx,H-B); x.stroke(); }
  588. // 塔共振禁区带
  589. if(P.resoDn&&P.resoUp){
  590. x.fillStyle='#e06c6c1f'; x.fillRect(sx(P.resoDn),16,sx(P.resoUp)-sx(P.resoDn),H-B-16);
  591. x.strokeStyle='#e06c6c88'; x.setLineDash([4,3]);
  592. x.strokeRect(sx(P.resoDn),16,sx(P.resoUp)-sx(P.resoDn),H-B-16); x.setLineDash([]);
  593. x.fillStyle='#e06c6c'; x.font='10px sans-serif'; x.fillText('塔共振禁区', sx(P.resoDn)+3, 28);
  594. }
  595. // 额定 / 超速 竖线
  596. [[P.ratedRpm,'#5aa9e6','额定'],[P.overSpd,'#e06c6c','超速']].forEach(([v,cc,lb])=>{
  597. x.strokeStyle=cc; x.setLineDash([5,4]); x.beginPath(); x.moveTo(sx(v),16); x.lineTo(sx(v),H-B); x.stroke(); x.setLineDash([]);
  598. x.fillStyle=cc; x.font='10px sans-serif'; x.fillText(lb, sx(v)-x.measureText(lb).width/2, H-B+13);
  599. });
  600. // 控制律曲线 (直接调 torqueLaw, 所以画的就是在跑的那条)
  601. x.strokeStyle='#4ec9a0'; x.lineWidth=2; x.beginPath();
  602. for(let r=0;r<=rMax;r+=8){ const q=torqueLaw(r); const px=sx(r),py=sy(q); r?x.lineTo(px,py):x.moveTo(px,py); }
  603. x.stroke(); x.lineWidth=1;
  604. // 轨迹
  605. if(S.hist.length>1){
  606. x.strokeStyle='#e0b34155'; x.beginPath();
  607. S.hist.forEach((h,i)=>{ const px=sx(h.rpm),py=sy(h.tq); i?x.lineTo(px,py):x.moveTo(px,py); });
  608. x.stroke();
  609. }
  610. // 工作点
  611. const opx=sx(S.rpm), opy=sy(S.tq);
  612. x.fillStyle=stCol(); x.beginPath(); x.arc(opx,opy,6,0,2*Math.PI); x.fill();
  613. x.strokeStyle='#0b0d11'; x.lineWidth=2; x.beginPath(); x.arc(opx,opy,6,0,2*Math.PI); x.stroke(); x.lineWidth=1;
  614. x.fillStyle='#8b93a3'; x.font='10px sans-serif';
  615. x.fillText('转矩 kNm', 6, 22); x.fillText('发电机转速 rpm', W-96, H-6);
  616. if(!MASK){ x.fillText('0', L-10, H-B+4);
  617. x.fillText((P.tqMax/1000).toFixed(0), L-26, sy(P.tqMax/1000)+4); }
  618. }
  619. function drawState(){
  620. const c=document.getElementById('cv-state'), x=c.getContext('2d');
  621. const W=c.width,H=c.height; x.fillStyle='#0b0d11'; x.fillRect(0,0,W,H);
  622. const groups=[{l:'刹车 0-6',ids:[0,1,2,3,4,5,6],c:'#e06c6c'},
  623. {l:'待机/自检',ids:[7,8,9,10,11,12,26],c:'#8b93a3'},
  624. {l:'启动',ids:[13],c:'#e0b341'},
  625. {l:'DFIG 死块',ids:[14,15,16,18],c:'#4a4f5c'},
  626. {l:'CAGE 实机',ids:[19,20,21,23],c:'#4ec9a0'},
  627. {l:'互切',ids:[24,25],c:'#a48ac8'}];
  628. let gx=8; x.font='9px sans-serif';
  629. groups.forEach(g=>{
  630. x.fillStyle=g.c; x.fillText(g.l,gx,11);
  631. g.ids.forEach((id,i)=>{
  632. const bx=gx+i*15, by=17;
  633. x.fillStyle = id===S.mode? g.c : g.c+'2e';
  634. x.fillRect(bx,by,13,13);
  635. x.fillStyle = id===S.mode? '#08121c':'#9aa2b1'; x.font='8px sans-serif';
  636. x.fillText(String(id), bx+(id<10?4:1), by+9); x.font='9px sans-serif';
  637. });
  638. gx += Math.max(g.ids.length*15, 56)+14;
  639. });
  640. x.fillStyle='#5a6070'; x.font='10px sans-serif';
  641. x.fillText('实测启动 13→19→20,不经 14-18 ⇒ DFIG 族恒不进入(软件预留)', 8, H-8);
  642. }
  643. function updateReadouts(){
  644. document.getElementById('v-ws').textContent=document.getElementById('in-ws').value;
  645. document.getElementById('v-ti').textContent=document.getElementById('in-ti').value;
  646. const M=F.operation_modes.modes;
  647. document.getElementById('v-mode').textContent =
  648. S.mode+' '+(M[S.mode]||'—')+(S.bp!==null?(' · 停机段'+(S.bpPhase+1)):'');
  649. }
  650. /* ── 机制状态: 紧凑 chip, 点开看详情 ── */
  651. function updateChips(){
  652. const tz=tezGate(S.rpm);
  653. const C=[
  654. ['M1 塔共振转速禁区', tz.inZone?'on':'off', `PLC 侧 ${fmt(P.resoDn,0)}–${fmt(P.resoUp,0)} rpm 生效 · RTW 侧 ${fmt(P.tezLo,0)}–${fmt(P.tezHi,0)} 未使能`],
  655. ['M2 塔阻尼 fore-aft', S.faActive?'on':'off', `走变桨 · 功率滞环 ${fmt(P.faOn,2)}/${fmt(P.faOff,2)}`],
  656. ['M2b 塔阻尼 side-side','dis',`走转矩 · Enable=${fmt(P.ssEnable,0)}`],
  657. ['M3 传动链扭振+陷波','on','定标未锚 · 只用比值与排序'],
  658. ['M4 最优增益查表',(S.mode===20&&S.rpm<P.ratedRpm)?'on':'off',`${MASK?'▪▪':(P.optGainX?P.optGainX.length:'—')} 点 · 自适应层出厂未学习`],
  659. ['M5 变桨 PI 增益调度',(S.pitch>0.2&&S.mode===20)?'on':'off',`Kp 随 β 线性 · β∈[${fmt(P.pitLo,0)},${fmt(P.pitHi,0)}]°`],
  660. ['M6 变桨速率分级', S.bp!==null?'on':'off',`Slow ${fmt(P.rSlow,1)} · Stop ${fmt(P.rStop,1)} · Fast ${fmt(P.rFast,1)} · Max ${fmt(P.rMax,1)}`],
  661. ['M7 刹车程序三套编号','on', pz('代码 7 档 / 码表 15 / 交付阶梯 24 —— 勿混')],
  662. ['M8 LVRT', S.lvrt>0?'on':'off','CAGE/DFIG 双分支 · 无 crowbar/chopper'],
  663. ['M9 变桨后备电池', S.batt?'on':'off','蓄能器仅自检层 · 监视≠可驱动'],
  664. ['M10 独立变桨 ALC','dis','4 接近探头 @45/135/225/315°'],
  665. ['M11 叶片摆振滤波', P.bladeEdge?'on':'off','版本增量 48 项之一'],
  666. ['M12 限电损失表', S.curtail>0?'on':'off', MASK?'功率损失查表(已隐)':'CON_CURTAIL_ET_LOSS / PWR'],
  667. ['M13 DFIG 分支','dead','符号层与 CAGE 逐条对称 ⇒ 活死只能靠运行数据判'],
  668. ];
  669. document.getElementById('chips').innerHTML = C.map(([n,st,d])=>
  670. `<div class="chip ${st}"><span class="dot"></span><span class="cn">${n}</span><span class="cd">${d}</span></div>`).join('');
  671. document.querySelectorAll('#chips .cd').forEach(e=>maskTextNodes(e));
  672. const noteEl=document.getElementById('note-map');
  673. if(noteEl) noteEl.innerHTML='绿线 = 正在跑的转矩律(直接调 <code>torqueLaw</code> 画出,非另画一条)· 黄迹 = 近 3 分钟工作点轨迹 · 红带 = 塔共振禁区';
  674. }
  675. /* ══════════════ 静态面板 ══════════════ */
  676. function buildStatic(){
  677. const I=F.identity.identity;
  678. const BL=F.baselines.baselines.shutdown_programs_measured;
  679. document.getElementById('hdr-id').innerHTML =
  680. I.platform.model+' · '+I.platform.rating+' · '+txt(I.platform.site)+
  681. ' &nbsp;<span class="badge b-tpl">参数=模板值</span>'+
  682. (MASK?' <span class="badge b-off">关键细节已隐去</span>':'');
  683. // 三系统
  684. const sub=document.getElementById('subsys'); if(sub) sub.innerHTML = `
  685. <h3>主控 <span class="badge b-tpl">模板值</span></h3>
  686. <div class="row"><span class="k">平台</span><span class="v">${I.controller.plc}</span></div>
  687. <div class="row"><span class="k">核控</span><span class="v">${I.controller.core}</span></div>
  688. <div class="row"><span class="k">额定转速</span><span class="v">${fmt(P.ratedRpm,0,'rpm')}</span></div>
  689. <h3>变桨 <span class="badge b-tpl">模板值</span></h3>
  690. <div class="row"><span class="k">型式</span><span class="v">${I.pitch.type}</span></div>
  691. <div class="row"><span class="k">总线</span><span class="v">${I.pitch.comm}</span></div>
  692. <div class="row"><span class="k">速率上限</span><span class="v">${fmt(P.rMax,2,'°/s')}</span></div>
  693. <div class="row"><span class="k">电池后备限幅</span><span class="v">${fmt(P.rBatt,2,'°/s')}</span></div>
  694. <h3>变频 <span class="badge b-tpl">模板值</span></h3>
  695. <div class="row"><span class="k">型号</span><span class="v">${I.converter.model}</span></div>
  696. <div class="row"><span class="k">固件</span><span class="v">${I.converter.fw}</span></div>
  697. <div class="row"><span class="k">LVRT 转矩限幅系数</span><span class="v">${fmt(P.lvrtClamp,3)}</span></div>
  698. <div class="note">★ 变桨后备驱动路径在代码里<b>只有电池一条</b>;蓄能器仅出现在自检状态码层(<code>SC_SELFTEST_PitchAccumulatorStatusError1..3</code>),<b>无经蓄能器驱动变桨的 POU 或分支</b> —— 与 EN15 的 <code>BNOPITCHDRIVEVIAACCUMULATOR</code> 不同构。</div>
  699. `;
  700. // 证据等级
  701. document.getElementById('evid').innerHTML = `
  702. <div class="row"><span class="k">包内自标</span><span class="v"><span class="badge b-tpl">模板值</span></span></div>
  703. <div class="note" style="border-top:none;margin-top:2px">
  704. <b>本包无一条够得上「实机回读」。</b>作分组依据/设计意图合格,<b>作阈值锚不合格</b>。
  705. 页内不出现「真值」二字。
  706. </div>
  707. <h3>可作行为锚的(调机期实测)</h3>
  708. <div class="row"><span class="k">BP51 卸载后顺桨</span><span class="v">${fmt(BL.BP51.pitch_phases_dps[1],4,'°/s')} <span class="badge b-meas">100Hz 三桨</span></span></div>
  709. <div class="row"><span class="k">BP51 卸载期</span><span class="v"><span class="badge b-ins">非恒速率</span></span></div>
  710. <div class="row"><span class="k">BP200 顺桨</span><span class="v">${fmt(BL.BP200?BL.BP200.pitch_phases_dps[0]:null,3,'°/s')} <span class="badge b-meas">10Hz 三桨</span></span></div>
  711. <div class="row"><span class="k">变桨阶跃速率</span><span class="v">${fmt(F.baselines.baselines.pitch_blade1_n43.rate_dps.up,2,'°/s')} <span class="badge b-meas">n=43 单叶</span></span></div>
  712. <div class="note">
  713. ${MASK?'<b>关键数值已隐去</b>(下列订正保留结论与方向,数字以 ▪▪ 代)<br>':''}
  714. ⚠ <b>三条订正随页走,不可略</b>:<br>
  715. ① 原记「卸载期 1.000 = <code>Con_PitchSlowRate</code>」在 repo 内保留的 K2 切片上<b>不可复现</b>(该事件内无 1.0 段,均值 0.831 且全程在变)。原结论建立在 3 个事件上,仅 1 个被复核 ⇒ <b>该腿降 INSUFFICIENT</b>,仅 <code>NormalStopRate</code> 腿经行为验证。<br>
  716. ② <b>「分段」不等于「每段都是恒速率」</b>。该过程实为 速率受限段(2.000, 参数 binding) + 一个前置的非速率受限段(疑转矩/功率受限,机制 INSUFFICIENT)。<br>
  717. ③ BP200 的 ${fmt(BL.BP200?BL.BP200.pitch_phases_dps[0]:null,3)} <b>超 <code>Con_PitchMaxRate</code>/<code>Con_PitchBatteryMaxRate</code> 约 14%</b>,且不等于 RTW 任何速率参数 ⇒ 该参数在安全链路径下<b>不 binding</b>(单场单事件,候选级)。原记该值出自「100Hz 桨角」系<b>出处错标</b>,实为 10Hz 段。<br>
  718. ④ 频率类参数(<code>Con_DRVDamper_Freq/Freq2</code>、<code>Con_NotchFilter_Freq</code>、<code>DTFrequency</code>)<b>定标未锚</b> —— 停机录波分窗测得 1.2–2.6 Hz 内峰位随转速漂移(2.004→1.822→1.324→1.211),是阶次内容非固定结构线 ⇒ <b>只用比值与排序,不换算</b>。<br>
  719. ⑤ <code>TowerFrequency</code> 的十倍频<b>已锚</b>:PLC 转速带 × 实测传动比 → 3P 上限对上 RTW 塔频,差 0.7%(跨参数体系闭合)。但<b>中心</b>差 3~4%,两套禁区各自反推的隐含塔频彼此只差 0.7% 却与参数差 3~4% ⇒ <b>闭合成立、但闭合到的可能不是该参数</b>,未决。
  720. </div>
  721. <h3>边界</h3>
  722. <div class="note" style="border-top:none">
  723. · 气动模型(Cp-λ-β)与折算惯量 J 是<b>仿真常数</b>,非 OEM 参数,不可外引。<br>
  724. · 调机期基线 = 轻载/无气动载,<b>只迁形态与相对判据,不迁绝对数值</b>;覆盖面 = 单台 K19/K2、单叶、n=43、2014-15。<br>
  725. · 本页所有参数 <b>运行时从同目录 JSON 读</b>,源码零内嵌。演示/上云开「公开模式」。
  726. </div>
  727. `;
  728. maskTextNodes(document.getElementById('evid'));
  729. maskTextNodes(document.getElementById('subsys'));
  730. }
  731. function updateMechs(){
  732. const on=(b)=>b?'active':'disabled';
  733. const tz=tezGate(S.rpm);
  734. const M=[
  735. {t:'M1 塔共振转速禁区(两套并存)', cls: tz.inZone?'active':'disabled',
  736. d:`PLC <code>PAR_rTowerResoGenSpeedDown/Up</code> = ${fmt(P.resoDn,0)} / ${fmt(P.resoUp,0)} rpm <span class="badge b-tpl">生效</span><br>
  737. RTW <code>TEZLowerRpm/TEZRpm/TEZUpperRpm</code> = ${fmt(P.tezLo,0)} / ${fmt(P.tezMid,0)} / ${fmt(P.tezHi,0)} rpm,
  738. <code>TEZ_Enable</code>=${fmt(P.tezEnable,0)} <span class="badge b-off">未使能</span><br>
  739. 当前 ${tz.inZone? '<b>在禁区内</b>,dwell '+S.tezTimer.toFixed(1)+'s':'不在禁区'}`},
  740. {t:'M2 塔架阻尼(双通道)', cls: S.faActive?'active':'disabled',
  741. d:`fore-aft 走<b>变桨</b>:<code>CON_PITCH_TWR_NUM1/2 DEN1/2 G</code> 二阶离散传函;功率滞环 投入 ${fmt(P.faOn,2)} / 退出 ${fmt(P.faOff,2)}<br>
  742. side-side 走<b>转矩</b>:<code>TwrSideSideTorq_Kp/Ti</code>,<code>Enable</code>=${fmt(P.ssEnable,0)} <span class="badge b-off">未使能</span><br>
  743. <code>TowerFrequency</code> = ${fmt(P.fTower,3,'Hz')} <span class="badge b-meas">十倍频已锚</span>`},
  744. {t:'M3 传动链扭振阻尼 + 陷波', cls:'active',
  745. d:`<code>Con_DRVDamper_{Freq,Freq2,K,K2}</code> 双模态 · <code>Con_NotchFilter_Freq</code> · <code>DTFrequency</code><br>
  746. <span class="badge b-ins">定标未锚</span> 停机录波峰位随转速漂移 ⇒ 阶次内容非结构线,<b>只用比值与排序</b>`},
  747. {t:'M4 额定以下 最优增益查表', cls: (S.mode===20&&S.rpm<P.ratedRpm)?'active':'disabled',
  748. d:`<code>CON_GENSPEED_OPTGAIN_X/Y</code> ${P.optGainX?P.optGainX.length:'—'} 点<br>
  749. 自适应层 <code>NV_compressed_RTW</code>(OptimalGainCorr/PitchOffset/Power 各 30 点风速格)<b>出厂全 1/全 0 = 未学习</b>`},
  750. {t:'M5 额定以上 PI 增益按桨角线性调度', cls: (S.pitch>0.2&&S.mode===20)?'active':'disabled',
  751. d:`Kp = ${fmt(P.kpInt,1)} + ${fmt(P.kpSlope,3)}·β,Ti = ${fmt(P.tiInt,1)} + ${fmt(P.tiSlope,3)}·β<br>
  752. 调度区间 β ∈ [${fmt(P.pitLo,0)}, ${fmt(P.pitHi,0)}]°`},
  753. {t:'M6 变桨速率分级', cls: S.bp!==null?'active':'disabled',
  754. d:`Slow ${fmt(P.rSlow,1)}(持续 ${fmt(P.rSlowDur,0)}s) · NormalStop ${fmt(P.rStop,1)} · Fast ${fmt(P.rFast,1)}(${fmt(P.rFastDur,0)}s) · Max ${fmt(P.rMax,1)} · Battery ${fmt(P.rBatt,1)} °/s<br>
  755. <span class="badge b-bad">Battery 与 Max 同值</span> ⇒ 参数独立存在但<b>取值不独立</b>,后备态不降速率`},
  756. {t:'M7 刹车程序 —— 三套编号并存,勿混', cls:'active',
  757. d:`代码 POU 档位 <b>7</b> 个(wbp 符号)· 码表设计列 <b>15</b> 个取值 · 交付阶梯 <b>24</b> 个取值<br>
  758. <b>代码有 ∧ 设计列无 = 空集</b>(SC-1 无「代码里有、码表里无」的档;EN15 的 198/30 无码属平台差异)<br>
  759. ⚠ 用错列会把 51/100 也算成「代码有码表无」,而它们分别有 316 / 95 条码`},
  760. {t:'M8 LVRT(双拓扑分支)', cls: S.lvrt>0?'active':'disabled',
  761. d:`主控侧仅 <code>LVRT_TorqLimitClampCoeff</code> = ${fmt(P.lvrtClamp,3)};<b>crowbar / chopper 包内零命中</b><br>
  762. 状态机 <code>CAGE_LVRT</code>(21) 与 <code>DFIG_LVRT</code>(16) <b>并列存在</b> ⇒ 两套穿越逻辑都在软件里`},
  763. {t:'M9 变桨后备(电池路径)', cls: S.batt?'active':'disabled',
  764. d:`<code>Con_PitchBatteryMaxRate</code>;每桨 <code>AI_BLADE{1,2,3}_BATTERYVOLTAGE</code> / <code>TEMPBATTERYBOX</code>(BATTERY 词根 225 条)<br>
  765. ACCUMULATOR 53 条中<b>仅 3 条与变桨相关且全是自检码</b> ⇒ 监视 ≠ 可驱动`},
  766. {t:'M10 独立变桨 ALC', cls:'disabled',
  767. d:`<code>ALCPitch_{Kp,Ti,tao,AzimOffset,MaxOffset}</code> · <code>IndPitch_MaxOffset</code> · 轮毂载荷 4 接近探头 @45/135/225/315°`},
  768. {t:'M11 叶片摆振滤波', cls: P.bladeEdge?'active':'disabled',
  769. d:`<code>CON_BladeEdge_{Num,Den,Mag,DP}</code> · <code>EnableBladeEdgeMonitor</code>=${fmt(P.bladeEdge,0)}<br>
  770. 属新旧版本增量 48 项之一(334 → 379 参数)`},
  771. {t:'M12 限电功率损失表', cls: S.curtail>0?'active':'disabled',
  772. d:`<code>CON_CURTAIL_ET_LOSS</code> / <code>CON_CURTAIL_PWR</code>(同属增量 48 项)`},
  773. {t:'M13 DFIG 分支 —— 软件预留,恒不进入', cls:'dead',
  774. d:`状态 14-18 + <code>Cage_DFIG_Enable</code>=${fmt(P.cageDfig,0)} + wbp 符号 11 条(与 CAGE 族 11 条<b>逐条一一对应</b>)<br>
  775. <b>符号层完全无法区分死块与活块</b>:前缀/后缀/PRG_ 块/三联参数全部对称 ⇒ 活死判别只能靠<b>运行数据的模式列直方图</b>`},
  776. ];
  777. document.getElementById('mechs').innerHTML = M.map(m=>
  778. `<div class="mech ${m.cls}"><div class="t">${m.t}</div><div class="d">${m.d}</div></div>`).join('');
  779. }
  780. /* ══════════════ 交互 ══════════════ */
  781. document.getElementById('btn-run').onclick = e=>{
  782. running=!running; e.target.classList.toggle('on',running);
  783. e.target.textContent = running?'⏸ 暂停':'▶ 运行';
  784. };
  785. document.getElementById('btn-reset').onclick = ()=>{ reset(); draw(); };
  786. document.getElementById('btn-mask').onclick = e=>{
  787. MASK=!MASK; e.target.classList.toggle('on',MASK);
  788. e.target.textContent = MASK?'🔒 已隐去关键细节':'🔓 内部模式(显全部)';
  789. buildStatic(); draw();
  790. };
  791. document.querySelectorAll('[data-bp]').forEach(b=> b.onclick=()=>{ trigBP(+b.dataset.bp); });
  792. document.querySelectorAll('[data-sc]').forEach(b=> b.onclick=()=>{
  793. const s=b.dataset.sc, ws=document.getElementById('in-ws');
  794. if(s==='gust'){ S.gust=8; soe('warn','注入阵风 +8 m/s'); }
  795. if(s==='ramp'){ ws.value=16; soe('info','风速爬坡至 16 m/s'); }
  796. if(s==='lull'){ ws.value=2.5; soe('info','风速跌至 2.5 m/s'); }
  797. });
  798. document.querySelectorAll('[data-inj]').forEach(b=> b.onclick=()=>{
  799. const k=b.dataset.inj;
  800. if(k==='lvrt'){ S.lvrt=1.5; soe('warn','电网跌落 · 转矩限幅系数投入'); }
  801. if(k==='battery'){ S.batt=!S.batt; soe('warn','变桨后备电池 '+(S.batt?'投入':'退出')); }
  802. if(k==='curtail'){ S.curtail = S.curtail>0?0:0.5; soe('warn','限电 '+(S.curtail>0?'50%':'解除')); }
  803. });
  804. function loop(ts){
  805. if(running){ for(let i=0;i<4;i++) step(); draw(); }
  806. requestAnimationFrame(loop);
  807. }
  808. boot().then(()=>requestAnimationFrame(loop));
  809. </script>