sc1_sim_demo.html 50 KB

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