| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868 |
- <meta charset="utf-8">
- <title>SC-1 / EN136 4MW 整机联调台</title>
- <style>
- :root{--bg:#0f1115;--fg:#e6e9ef;--dim:#8b93a3;--line:#232733;--card:#161a22;
- --ok:#4ec9a0;--warn:#e0b341;--bad:#e06c6c;--off:#5a6070;--acc:#5aa9e6}
- *{box-sizing:border-box}
- body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.55 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
- header{padding:12px 16px;border-bottom:1px solid var(--line);display:flex;gap:14px;align-items:baseline;flex-wrap:wrap}
- h1{font-size:15px;margin:0;font-weight:600}
- .sub{color:var(--dim);font-size:12px}
- main{display:grid;grid-template-columns:274px 1fr 400px;gap:12px;padding:12px;align-items:start}
- @media(max-width:1100px){main{grid-template-columns:1fr}}
- .card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:12px}
- .card h2{font-size:12px;margin:0 0 8px;color:var(--dim);font-weight:600;letter-spacing:.04em}
- .card h3{font-size:12px;margin:10px 0 5px;font-weight:600}
- .row{display:flex;justify-content:space-between;gap:8px;padding:2px 0;font-variant-numeric:tabular-nums}
- .row b{font-weight:600}
- .k{color:var(--dim)}
- .v{font-variant-numeric:tabular-nums}
- canvas{width:100%;height:auto;display:block;background:#0b0d11;border-radius:6px;border:1px solid var(--line)}
- 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}
- button:hover{border-color:var(--acc)}
- button.on{background:var(--acc);color:#08121c;border-color:var(--acc)}
- .btns{display:flex;gap:5px;flex-wrap:wrap;margin:6px 0}
- input[type=range]{width:100%}
- .badge{display:inline-block;font-size:10px;padding:1px 5px;border-radius:3px;border:1px solid var(--line);color:var(--dim);vertical-align:middle}
- .b-tpl{border-color:#4a4030;color:#c8a45a}
- .b-meas{border-color:#2e4a3c;color:var(--ok)}
- .b-off{border-color:#3a3d47;color:var(--off)}
- .b-bad{border-color:#4a2e2e;color:var(--bad)}
- .b-ins{border-color:#3a3446;color:#a48ac8}
- .mech{border-left:2px solid var(--line);padding-left:9px;margin:9px 0}
- .mech.active{border-left-color:var(--ok)}
- .mech.disabled{border-left-color:var(--off);opacity:.62}
- .mech.dead{border-left-color:var(--bad);opacity:.5}
- .mech .t{font-weight:600;font-size:12px}
- .mech .d{color:var(--dim);font-size:11px;margin-top:2px}
- #soe{height:190px;overflow-y:auto;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.5}
- #soe div{border-bottom:1px solid #1a1e27;padding:1px 0}
- .t-info{color:var(--dim)} .t-warn{color:var(--warn)} .t-stop{color:var(--bad)} .t-ok{color:var(--ok)}
- .note{color:var(--dim);font-size:11px;margin-top:6px;border-top:1px dashed var(--line);padding-top:6px}
- .err{background:#2a1a1a;border:1px solid #4a2e2e;color:#f0b8b8;padding:10px;border-radius:6px;margin:12px}
- table{width:100%;border-collapse:collapse;font-size:11px;font-variant-numeric:tabular-nums}
- td,th{border-bottom:1px solid var(--line);padding:3px 4px;text-align:left}
- th{color:var(--dim);font-weight:600}
- code{background:#1b2029;padding:1px 4px;border-radius:3px;font-size:11px}
- .masked{color:var(--off);letter-spacing:2px}
- .chip{display:flex;align-items:baseline;gap:6px;padding:3px 0;border-bottom:1px solid #171b23}
- .chip .dot{width:7px;height:7px;border-radius:50%;flex:0 0 7px;background:var(--off)}
- .chip.on .dot{background:var(--ok);box-shadow:0 0 6px var(--ok)}
- .chip.off .dot{background:#39404e}
- .chip.dis .dot{background:#39404e;border:1px solid #4a4f5c}
- .chip.dead .dot{background:var(--bad);opacity:.55}
- .chip .cn{font-size:11px;font-weight:600;flex:0 0 auto}
- .chip .cd{font-size:10px;color:var(--dim);flex:1;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
- .chip.on .cn{color:var(--ok)} .chip.dead .cn{color:var(--bad);opacity:.75}
- #soe{height:150px}
- </style>
- <header>
- <h1>SC-1 / EN136 4MW 整机联调台</h1>
- <span class="sub" id="hdr-id">载入中…</span>
- <span style="flex:1"></span>
- <span class="btns" style="margin:0">
- <button id="btn-run">▶ 运行</button>
- <button id="btn-reset">重置</button>
- <button id="btn-mask">🔓 内部模式(显全部)</button>
- </span>
- </header>
- <div id="boot" class="err" style="display:none"></div>
- <main id="app" style="display:none">
- <div>
- <div class="card">
- <h2>风况台</h2>
- <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>
- <input type="range" id="in-ws" min="0" max="28" step="0.5" value="8">
- <div class="row"><span class="k">湍流强度</span><span class="v"><b id="v-ti">10</b> %</span></div>
- <input type="range" id="in-ti" min="0" max="30" step="1" value="10">
- <div class="btns">
- <button data-sc="gust">阵风 +8</button>
- <button data-sc="ramp">爬到 16 m/s</button>
- <button data-sc="lull">跌到 2.5 m/s</button>
- </div>
- </div>
- <div class="card">
- <h2>停机程序 · 故障注入</h2>
- <div class="btns">
- <button data-bp="50">低风停机</button><button data-bp="51">常规停机</button>
- <button data-bp="100">快速停机</button><button data-bp="200">过速继电器</button>
- <button data-bp="210">急停</button>
- </div>
- <div class="btns">
- <button data-inj="lvrt">电网跌落</button><button data-inj="battery">电池后备</button>
- <button data-inj="curtail">限电 50%</button>
- </div>
- </div>
- <div class="card">
- <h2>机制状态</h2>
- <div id="chips"></div>
- </div>
- <div class="card">
- <h2>SOE 事件序列</h2>
- <div id="soe"></div>
- </div>
- </div>
- <div>
- <div class="card">
- <h2>正视 · 转子与桨距<span class="k" style="font-weight:400"> — 叶片按真实转速转、按真实桨距扭</span></h2>
- <canvas id="cv-rotor" width="900" height="470"></canvas>
- </div>
- <div class="card">
- <h2>功率流<span class="k" style="font-weight:400"> — 风 → 气动 → 传动 → 发电机 → 变频 → 电网</span></h2>
- <canvas id="cv-flow" width="900" height="130"></canvas>
- </div>
- </div>
- <div>
- <div class="card">
- <h2>控制律工作点<span class="k" style="font-weight:400"> — 转矩-转速平面</span></h2>
- <canvas id="cv-map" width="620" height="420"></canvas>
- <div class="note" id="note-map"></div>
- </div>
- <div class="card">
- <h2>运行状态机</h2>
- <canvas id="cv-state" width="620" height="96"></canvas>
- <div class="row" style="margin-top:4px"><span class="k">当前</span><span class="v" id="v-mode">—</span></div>
- </div>
- <div class="card">
- <h2>证据等级与边界</h2>
- <div id="evid"></div>
- </div>
- </div>
- </main>
- <script src="./sc1_sim_params.js" onerror="window.__noParamsJs=1"></script>
- <script>
- 'use strict';
- /* ══════════════════════════════════════════════════════════════════
- SC-1 / EN136 4MW 整机联调台
- ★ 本文件源码内**不含任何 OEM 数值**。所有参数在运行时从同目录 JSON 读取:
- control_law_params_rtw.json (RTW 核控层, 379 参数)
- param_dict.json (PLC PAR_* 层, 765 参数)
- baselines.json (调机期实测基线)
- identity.json / operation_modes.json / bp_semantics.json
- ⇒ 需经 http 同源提供 (在本目录跑 `python3 -m http.server` 后打开),
- file:// 直开会被浏览器 CORS 拦住 —— 这是刻意的, 不是缺陷。
- ★ 参数集敏感度: internal · OEM 控制律核心机密。演示/上云一律开"公开模式"。
- ══════════════════════════════════════════════════════════════════ */
- const F = {}; // 加载后的原始 JSON
- let P = {}; // 解析后的数值参数
- let MASK = false; // ★内部工程版: 默认全展开(最详细)。掩码按钮供临时截图/外传时用。
- // 对外演示走 sc1_sim_demo.html(替代参数, 文件内零 OEM 数值)。
- const num = v => { const x = parseFloat(v); return Number.isFinite(x) ? x : null; };
- const arr = v => Array.isArray(v) ? v.map(num) : null;
- /* ── 载入 ───────────────────────────────────────────── */
- async function boot(){
- const files = ['control_law_params_rtw.json','param_dict.json','baselines.json',
- 'identity.json','operation_modes.json','bp_semantics.json'];
- const need = files.map(f=>f.replace('.json',''));
- // 路径 A: <script src="./sc1_sim_params.js"> 注入 —— file:// 双击即可用 (script 标签不受 CORS 限制)
- if(window.SC1_PARAMS && need.every(k=>window.SC1_PARAMS[k])){
- need.forEach(k=>F[k]=window.SC1_PARAMS[k]);
- } else
- // 路径 B: http 同源 fetch —— 改了 JSON 后无需重新打包, 取最新
- try{
- const res = await Promise.all(files.map(f => fetch('./'+f).then(r=>{
- if(!r.ok) throw new Error(f+' HTTP '+r.status); return r.json();
- })));
- files.forEach((f,i)=>F[f.replace('.json','')] = res[i]);
- }catch(e){
- document.getElementById('boot').style.display='block';
- document.getElementById('boot').innerHTML =
- '<b>参数未载入</b> — '+e.message+
- '<br><br>本页刻意不把 OEM 数值内嵌进 HTML(参数集标 <code>internal · OEM 控制律核心机密</code>),'+
- '改从<b>同目录的独立文件</b>取。两条路任一即可:'+
- '<br>① <b>直接双击打开</b>:确保同目录有 <code>sc1_sim_params.js</code>'+
- '(没有就跑 <code>python3 scripts/build_sc1_sim_params.py</code> 生成)'+
- '<br>② <b>起个本地服务</b>:<code>cd resources/oem_envision_sc1_rudong2014 && python3 -m http.server 8791</code>'+
- ' 然后开 <code>http://localhost:8791/sc1_sim.html</code>';
- return;
- }
- parseParams();
- buildStatic();
- document.getElementById('app').style.display='grid';
- reset(); draw();
- }
- function parseParams(){
- const R = F.control_law_params_rtw, D = F.param_dict.params;
- const g = k => num(R[k]);
- const gp = k => num((D[k]||{}).golden_kk_2014_12 ?? (D[k]||{}).default);
- P = {
- // 转速链
- ratedRpm:g('GenSpd_RatedLoadSpeed'), minLoadCage:g('GenSpd_MinLoadSpeed_Cage'),
- overSpd:g('GenSpd_OverSpeed'), constSpd:g('GenSpd_ConstantSpeed'), speedUp:g('GenSpd_Speedup'),
- // 转矩
- tqMax:g('Con_MaxTorque'), tqMin:g('Con_MinTorque'), tqRamp:g('Con_TorqueMaxRampRate'),
- optGainX:arr(R['CON_GENSPEED_OPTGAIN_X']), optGainY:arr(R['CON_GENSPEED_OPTGAIN_Y']),
- optimalGain:g('OptimalGain'),
- // 额定以上变桨 PI 增益调度 (按桨角线性)
- kpSlope:g('Con_PitchAboveRated_KpSlope'), kpInt:g('Con_PitchAboveRated_KpInterCept'),
- tiSlope:g('Con_PitchAboveRated_TiSlope'), tiInt:g('Con_PitchAboveRated_TiInterCept'),
- pitLo:g('Con_PitchAboveRated_LowerPitchAngle'), pitHi:g('Con_PitchAboveRated_UpperPitchAngle'),
- // 变桨速率族
- rSlow:g('Con_PitchSlowRate'), rSlowDur:g('Con_PitchSlowRateDuration'),
- rStop:g('Con_PitchNormalStopRate'), rFast:g('Con_PitchFastRate'), rFastDur:g('Con_PitchFastRateDuration'),
- rMax:g('Con_PitchMaxRate'), rBatt:g('Con_PitchBatteryMaxRate'),
- // 塔共振禁区: PLC 侧生效 / RTW 侧 TEZ
- tezLo:g('TEZLowerRpm'), tezMid:g('TEZRpm'), tezHi:g('TEZUpperRpm'),
- tezHyst:g('TEZ_Hyst'), tezEnable:g('TEZ_Enable'),
- resoUp:gp('PAR_rTowerResoGenSpeedUp'), resoDn:gp('PAR_rTowerResoGenSpeedDown'),
- // 塔阻尼
- fTower:g('TowerFrequency'), twrG:g('CON_PITCH_TWR_G'),
- ssEnable:g('TwrSideSideTorq_Enable'), ssKp:g('TwrSideSideTorq_Kp'),
- faOn:g('TwrFbkForeAftPowerPctOn'), faOff:g('TwrFbForeAftkPowerPctOff'),
- // 传动链
- dtF1:g('Con_DRVDamper_Freq'), dtF2:g('Con_DRVDamper_Freq2'),
- dtK1:g('Con_DRVDamper_K'), dtK2:g('Con_DRVDamper_K2'),
- notch:g('Con_NotchFilter_Freq'), dtFreq:g('DTFrequency'),
- // 其他
- lvrtClamp:g('LVRT_TorqLimitClampCoeff'), ts:g('Ts'),
- bladeEdge:g('EnableBladeEdgeMonitor'), cageDfig:g('Cage_DFIG_Enable'),
- };
- const I = F.identity.identity;
- P.ratio = I.drivetrain.ratio_gen_rotor;
- const mkw = String(I.generator.rating||'').match(/(\d+(?:\.\d+)?)\s*kW/i);
- P.ratedKW = mkw ? parseFloat(mkw[1]) : null; // 从 identity.generator.rating 解析, 不硬编码
- P.torsFn = I.drivetrain.torsional_fn_hz;
- const BLm = F.baselines.baselines.shutdown_programs_measured || {};
- P.feather = (BLm.BP100 && BLm.BP100.feather_pos_deg) ?? null; // 顺桨到位角, 取自实测基线
- P.hwLimit = (I.pitch.hw_limits_deg||[])[0] ?? null; // 硬限位, 取自 identity
- }
- /* ── 掩码 ───────────────────────────────────────────── */
- function fmt(v, d=2, unit=''){
- if(v===null||v===undefined) return '<span class="masked">—</span>';
- if(MASK) return '<span class="masked">▪▪▪</span>'+(unit?' '+unit:'');
- return (typeof v==='number' ? (Math.abs(v)>=1000? v.toFixed(0) : v.toFixed(d)) : v)+(unit?' '+unit:'');
- }
- /* 掩码不止数值 —— 参数标识名同样泄露控制架构; 场名/业主属客户信息。
- 露出的只保留: 机制名称、功能有无、行为形态。 */
- function pn(name){ return MASK ? '<span class="masked">▪▪▪▪</span>' : '<code>'+name+'</code>'; }
- function txt(s){ return MASK ? '<span class="masked">▪▪▪</span>' : s; }
- /* 文本节点级掩码: 只改文字, 不碰标签与属性 —— 用于正文段落里散落的分析数字。
- 保留句子结构与结论方向, 只把数字与参数标识顶掉。 */
- function maskTextNodes(root){
- if(!MASK || !root) return;
- const w=document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
- const hit=[]; let n;
- while(n=w.nextNode()) hit.push(n);
- hit.forEach(n=>{
- n.nodeValue = n.nodeValue
- .replace(/-?\d+(?:[.,]\d+)*\s*(?:°\/s|℃|Hz|rpm|kW|kNm|%|°|s\b)?/g, m=>/^\s*$/.test(m)?m:'▪▪')
- .replace(/[A-Z][A-Za-z0-9_]{6,}/g, '▪▪▪▪');
- });
- root.querySelectorAll('code').forEach(c=>{ c.textContent='▪▪▪▪'; });
- }
- /* 正文里的散落数字: 掩码时整段用 ▪ 顶掉数字, 保留句子结构 */
- function pz(s){ return MASK ? s.replace(/-?\d+(?:\.\d+)?%?/g,'▪▪') : s; }
- /* ══════════════ 仿真核心 ══════════════ */
- const DT = 0.05;
- const WS_START = 3.5, WS_STOP = 3.0; // 起停风门 = 仿真常数, 非 OEM 参数
- let S, running=false, tPrev=0;
- function reset(){
- S = { t:0, ws:parseFloat(document.getElementById('in-ws').value), wsInst:8,
- rpm:0, tq:0, pitch:90, pwr:0, mode:7, bp:null, bpT:0, bpPhase:0,
- tezTimer:0, tezDir:0, lvrt:0, batt:false, curtail:0,
- faActive:false, integ:0, hist:[] };
- document.getElementById('soe').innerHTML='';
- soe('info','联调台重置 · 状态 7 CON_iStandby');
- }
- function soe(cls, msg){
- const el=document.getElementById('soe');
- const d=document.createElement('div');
- d.className='t-'+cls;
- d.textContent='['+S.t.toFixed(1).padStart(6)+'s] '+msg;
- el.insertBefore(d, el.firstChild);
- while(el.children.length>150) el.removeChild(el.lastChild);
- }
- // 转矩律: 额定以下最优增益查表 (RTW CON_GENSPEED_OPTGAIN_X/Y), 额定以上恒功率
- function torqueLaw(rpm){
- if(!P.optGainX||!P.optGainY) return 0;
- const X=P.optGainX, Y=P.optGainY;
- if(rpm<=X[0]) return P.optimalGain*rpm*rpm*1e-3;
- const constPwr = Math.min(P.tqMax, P.ratedKW*1000/(Math.max(rpm,1)*Math.PI/30))/1000;
- if(rpm>=P.ratedRpm) return constPwr;
- let k=Y[Y.length-1];
- for(let i=0;i<X.length-1;i++) if(rpm>=X[i]&&rpm<X[i+1]){
- const f=(rpm-X[i])/(X[i+1]-X[i]); k=Y[i]+f*(Y[i+1]-Y[i]); break;
- }
- const optTq = P.optimalGain*k*rpm*rpm*1e-3;
- // 额定前 60 rpm 内线性过渡到恒功率支 —— 否则转速在额定点上下抖时转矩指令在两支间跳变
- // (首版实测: 转矩曲线高频抖振; 这是控制律接缝, 非湍流)
- const band = 60;
- if(rpm > P.ratedRpm-band){
- const f=(rpm-(P.ratedRpm-band))/band;
- return optTq*(1-f) + constPwr*f;
- }
- return optTq;
- }
- // 塔共振禁区: PLC TowerReso 带生效; RTW TEZ 按 TEZ_Enable
- function tezGate(rpm){
- const lo=P.resoDn, hi=P.resoUp;
- if(lo===null||hi===null) return {inZone:false};
- const inZone = rpm>lo && rpm<hi;
- return {inZone, lo, hi};
- }
- // 塔架 fore-aft 阻尼: 走变桨通道, 按功率百分比滞环投入/退出
- function faDamper(pwrPct){
- if(P.faOn===null) return false;
- if(!S.faActive && pwrPct>P.faOn) { S.faActive=true; soe('ok','塔架 fore-aft 阻尼 投入 (功率 > 投入门)'); }
- if( S.faActive && pwrPct<P.faOff){ S.faActive=false; soe('info','塔架 fore-aft 阻尼 退出 (功率 < 退出门)'); }
- return S.faActive;
- }
- // 停机程序: 按 baselines.shutdown_programs_measured 的逐段实测速率
- function bpPitchRate(){
- const M = F.baselines.baselines.shutdown_programs_measured || {};
- const k = 'BP'+S.bp, m = M[k];
- if(S.bp===200 || S.bp===210){
- // 安全链路径: 用 BP200 的逐段实测 (baselines.shutdown_programs_measured.BP200)
- const m200 = M['BP200'];
- if(m200 && m200.pitch_phases_dps) return m200.pitch_phases_dps[0];
- return P.rMax; // 无实测则退回参数限幅, 不硬编码
- }
- if(!m || !m.pitch_phases_dps) return P.rStop ?? 2;
- const ph = m.pitch_phases_dps, win = m.pitch_phase_windows_s;
- if(win){
- for(let i=0;i<win.length;i++){
- const [a,b]=String(win[i]).split('-').map(Number);
- if(S.bpT>=a && S.bpT<b) { S.bpPhase=i; return ph[i]; }
- }
- S.bpPhase=ph.length-1; return ph[ph.length-1];
- }
- // BP51: 卸载期 → 卸载后。卸载完成以转矩跌破触发时刻的 5% 判 (与 K2 录波同口径)
- if(S.bpTq0===undefined || S.bpT<=DT) S.bpTq0 = Math.max(S.tq, 1e-6);
- const unloaded = S.tq < 0.05*S.bpTq0;
- S.bpPhase = unloaded?1:0;
- return unloaded ? ph[1] : ph[0];
- }
- function step(){
- // 风
- const ti=parseFloat(document.getElementById('in-ti').value)/100;
- S.ws = parseFloat(document.getElementById('in-ws').value);
- S.wsInst += ((S.ws-S.wsInst)*0.02) + (Math.random()-0.5)*S.ws*ti*0.25;
- S.wsInst = Math.max(0,S.wsInst);
- if(S.gust>0){ S.wsInst += S.gust; S.gust*=0.97; if(S.gust<0.05) S.gust=0; }
- const M = F.operation_modes.modes;
- /* 状态机 */
- if(S.bp!==null){ // 停机程序执行中
- S.bpT += DT;
- const r = bpPitchRate();
- S.pitch = Math.min(P.feather ?? P.hwLimit, S.pitch + r*DT);
- if(S.bp>=100) S.tq = Math.max(0, S.tq - (P.tqMax||3e4)/1000*DT/0.4); // 转矩切断 0.4s
- else S.tq = Math.max(0, S.tq - (P.tqMax||3e4)/1000*DT/16.8); // 速度环卸载 ~16.8s
- if(P.feather && S.pitch>=P.feather-0.1 && S.rpm<10){
- soe('ok','停机程序完成 · 顺桨到位 '+(MASK?'▪▪':S.bpT.toFixed(1))+'s · 转速归零'); S.bp=null; S.mode=7; S.bpTq0=undefined;
- } else if(S.bpT>600){
- soe('warn','停机程序超 600s 未完成 · 顺桨 '+S.pitch.toFixed(1)+'° 转速 '+S.rpm.toFixed(0)+' rpm — 如实报, 不假装完成');
- S.bp=null; S.bpTq0=undefined;
- }
- } else {
- // 常规运行链
- if(S.mode===7 && S.wsInst>WS_START){ S.mode=13; soe('info','13 CON_iStartMode · 开桨'); }
- if(S.mode===13){
- const openRate = F.baselines.baselines.startup_seq_n2.mode13_open_rate_dps || -2.0;
- S.pitch = Math.max(0, S.pitch + openRate*DT);
- const cut = (F.baselines.baselines.startup_seq_n2.cutin_gen_rpm||[820,840])[0];
- if(S.rpm>cut){ S.mode=19; soe('ok','19 CON_iCAGE_GutIn · 全功率软切入'); S.modeT=0; }
- }
- if(S.mode===19){ S.modeT=(S.modeT||0)+DT; if(S.modeT>0.2){ S.mode=20; soe('ok','20 CON_iCAGE_GridOp · 并网'); } }
- if(S.mode===7 && S.wsInst<WS_STOP) S.pitch = Math.min(90, S.pitch+1.0*DT);
- }
- /* 气动 → 转速
- ⚠ 以下 Cp(λ,β) 经验式、转子半径、折算惯量 J 均为**仿真常数, 非 OEM 参数**, 不可外引。
- 用 Cq=Cp/λ 算转矩而非 P/ω: 后者在 ω→0 时发散, 会让机组永远自启动不了(首版实测卡在 95 rpm)。 */
- const R = 68; // EN136 → 转子半径 (型号代号推得, 非包内参数)
- const rho = 1.225, A = Math.PI*R*R, J = 620; // J = 折算到高速轴的等效惯量 (仿真常数)
- const wr = (S.rpm/P.ratio)*Math.PI/30; // 转子角速度 rad/s
- const lam = wr*R/Math.max(S.wsInst,0.5);
- const b1 = S.pitch;
- const li = 1/( 1/(lam+0.08*b1+1e-6) - 0.035/(b1*b1*b1+1) );
- let cp = 0.5176*(116/li - 0.4*b1 - 5)*Math.exp(-21/li) + 0.0068*lam;
- cp = Math.max(0, Math.min(0.48, cp));
- const cq = lam>0.15 ? cp/lam : cp/0.15; // 转矩系数, λ→0 不发散
- const Qrot = 0.5*rho*A*R*S.wsInst*S.wsInst*cq/1000; // kNm (转子侧)
- const Taero = Qrot/P.ratio; // kNm (折算到高速轴)
- const Paero = 0.5*rho*A*Math.pow(S.wsInst,3)*cp/1000; // kW
- /* 转矩指令 */
- if(S.bp===null && S.mode===20){
- let cmd = torqueLaw(S.rpm);
- // 额定以上: 变桨 PI 按桨角线性增益调度
- if(S.rpm>P.ratedRpm || S.pitch>0.2){
- const kp = (P.kpInt??0) + (P.kpSlope??0)*Math.min(Math.max(S.pitch,P.pitLo??0),P.pitHi??24);
- const ti = (P.tiInt??10) + (P.tiSlope??0)*S.pitch;
- const err = S.rpm - P.ratedRpm;
- /* 抗饱和 (实跑逮到的 bug): 并网发生在远低于额定的转速上, err 长期为大负值 ⇒
- 积分器卷到 -248 并锁死, 之后在额定附近 ±50 rpm 摆, 最终被湍流推过超速阈跳 BP200。
- 首版"稳态 1546-1551"是观察窗不够长, 不是通过 —— 窗口短会把发散读成稳定。
- 两道: ①桨距贴在 0° 下限且误差仍为负 ⇒ 停止继续下卷 ②积分项硬限幅。 */
- const railed = (S.pitch <= 0.001 && err < 0);
- if(!railed) S.integ += err*DT/Math.max(ti,0.1);
- const IMAX = 200;
- S.integ = Math.max(-IMAX, Math.min(IMAX, S.integ));
- /* ⚠ KSCALE 是**仿真标度, 非 OEM 量**: RTW 的 Kp/Ti 单位未锚(与 Con_DRVDamper_Freq
- 等频率类同属"定标未锚"一族), 无法把它们换算成 °/s per rpm。这里用一个拟合标度让
- 闭环行为合理, 因此**本页的增益绝对值不可外引**, 只有"Kp 随桨角线性增大"这个**形状**
- 来自参数集。KSCALE 由 12 m/s 长窗压力测试定(该点是额定过渡, 最难)。 */
- const KSCALE = 0.0032;
- let pitCmd = (kp*err + S.integ)*KSCALE + (S.rpm-(S.rpmPrev??S.rpm))/DT*0.02;
- S.rpmPrev = S.rpm;
- if(faDamper(S.pwr/P.ratedKW)) pitCmd += Math.sin(2*Math.PI*(P.fTower||0.315)*S.t)*(P.twrG||0)*0.02;
- const lim = (S.batt ? P.rBatt : P.rMax) ?? 0; // 无参数则不动桨, 不编造限幅
- S.pitch = Math.max(0, Math.min(P.feather ?? P.hwLimit, S.pitch + Math.max(-lim,Math.min(lim,pitCmd))*DT));
- } else { S.integ=0; }
- if(S.curtail>0) cmd = Math.min(cmd, cmd*(1-S.curtail));
- if(S.lvrt>0){ cmd *= (P.lvrtClamp??0.2); S.lvrt-=DT; if(S.lvrt<=0) soe('ok','LVRT 结束 · 转矩限幅解除'); }
- S.tq += Math.max(-(P.tqRamp||500)*DT, Math.min((P.tqRamp||500)*DT, cmd-S.tq));
- }
- /* 阻力项 (仿真常数, 非 OEM): 轴承摩擦 + 风阻 + 顺桨叶片气动阻力。
- 缺了它顺桨到位后转速会永远挂住 —— 首版实测停机程序跑不完(完成=false)。 */
- const wg = S.rpm*Math.PI/30;
- const featherDrag = S.pitch>60 ? 0.02*(S.pitch-60)/30 : 0; // 顺桨段附加阻力
- const Qdrag = (0.004 + featherDrag)*wg + 2e-5*wg*wg; // kNm, 恒为制动方向
- /* 转速积分: dω/dt = (Q_aero − Q_em − Q_drag)/J , ω[rad/s] → rpm */
- const dw = (Taero - S.tq - Qdrag)*1000/J; // rad/s²
- S.rpm = Math.max(0, S.rpm + dw*DT*30/Math.PI);
- S.pwr = Math.max(0, S.tq*1000*(S.rpm*Math.PI/30)/1000);
- /* 塔共振禁区 dwell 计时 */
- const tz = tezGate(S.rpm);
- if(tz.inZone){
- S.tezTimer += DT;
- const maxT = num((F.param_dict.params['PAR_tResonanceModeTimeMax']||{}).golden_kk_2014_12)/1000;
- if(maxT && S.tezTimer>maxT && !S.tezWarned){
- soe('warn','共振区停留超时 · PAR_tResonanceModeTimeMax'); S.tezWarned=true;
- }
- } else { S.tezTimer=0; S.tezWarned=false; }
- /* 超速保护 */
- if(P.overSpd && S.rpm>P.overSpd && S.bp===null){
- soe('stop','超速 · 触发 BP200 (安全链 过速继电器)'); trigBP(200);
- }
- S.t += DT;
- if(S.hist.length===0 || S.t-S.hist[S.hist.length-1].t > 0.2)
- S.hist.push({t:S.t, rpm:S.rpm, tq:S.tq, pitch:S.pitch, pwr:S.pwr});
- if(S.hist.length>900) S.hist.shift();
- }
- function trigBP(n){
- S.bp=n; S.bpT=0; S.bpPhase=0;
- const lad = (F.bp_semantics.ladder||[]).find(x=>x.bp===n);
- const inCode = [0,50,51,100,198,200,210].includes(n);
- soe('stop',(MASK?'停机程序':'BP'+n)+' 触发 · 代码 POU '+(inCode?'有':'无')+' / 交付阶梯 '+(lad?'有':'无')
- +(inCode&&!lad?' ← 三套编号并存的实例, 非错误':''));
- }
- /* ══════════════ 绘制 ══════════════ */
- function draw(){ drawRotor(); drawFlow(); drawMap(); drawState(); updateReadouts(); updateChips(); }
- function stateColor(){
- if(S.bp!==null) return '#e06c6c';
- if(S.mode===20) return '#4ec9a0';
- if(S.mode===13||S.mode===19) return '#e0b341';
- return '#6b7near';
- }
- function stCol(){ return S.bp!==null?'#e06c6c' : S.mode===20?'#4ec9a0' : (S.mode===13||S.mode===19)?'#e0b341':'#6b7280'; }
- /* ── 正视风机: 塔+机舱+三桨, 按真实转速转、按真实桨距扭 ── */
- function drawRotor(){
- const c=document.getElementById('cv-rotor'), x=c.getContext('2d');
- const W=c.width,H=c.height; x.clearRect(0,0,W,H);
- const g=x.createLinearGradient(0,0,0,H); g.addColorStop(0,'#0c1018'); g.addColorStop(1,'#0a0d13');
- x.fillStyle=g; x.fillRect(0,0,W,H);
- const hubX=W*0.56, hubY=H*0.40, Rb=H*0.33; // 叶片长度(像素)
- const col=stCol();
- /* 风矢: 左侧箭头, 长度 ∝ 风速 */
- const wsN=Math.min(S.wsInst/25,1);
- x.strokeStyle='#3d5a80'; x.fillStyle='#3d5a80'; x.lineWidth=1.5;
- for(let k=0;k<6;k++){
- const yy=H*0.12+k*H*0.13, L=30+wsN*120;
- x.beginPath(); x.moveTo(20,yy); x.lineTo(20+L,yy); x.stroke();
- 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();
- }
- x.fillStyle='#8b93a3'; x.font='12px sans-serif';
- x.fillText('风 '+(MASK?'▪▪':S.wsInst.toFixed(1))+' m/s', 20, H*0.06);
- /* 塔筒 (锥形) */
- const base=H*0.98, tw0=26, tw1=13;
- x.fillStyle='#252b36';
- x.beginPath(); x.moveTo(hubX-tw0/2,base); x.lineTo(hubX-tw1/2,hubY+14);
- x.lineTo(hubX+tw1/2,hubY+14); x.lineTo(hubX+tw0/2,base); x.closePath(); x.fill();
- /* 机舱 */
- x.fillStyle='#2e3542'; x.fillRect(hubX-16,hubY-15,58,30);
- x.fillStyle='#1c212b'; x.fillRect(hubX+30,hubY-9,14,18);
- /* 三桨 */
- S.azi = (S.azi||0) + (S.rpm/P.ratio)*Math.PI/30*DT*4;
- for(let b=0;b<3;b++){
- const a=S.azi + b*2*Math.PI/3;
- x.save(); x.translate(hubX,hubY); x.rotate(a);
- // 叶片平面形: 根部窄→中段宽→梢部收
- x.fillStyle=col; x.globalAlpha=.88;
- x.beginPath();
- x.moveTo(-6,-8); x.quadraticCurveTo(-13,-Rb*0.45,-7,-Rb*0.92);
- x.lineTo(-1,-Rb); x.lineTo(4,-Rb*0.92);
- x.quadraticCurveTo(11,-Rb*0.45,7,-8); x.closePath(); x.fill();
- x.globalAlpha=1;
- // 70% 展向的翼型截面: 按桨距角扭转 —— 这是"桨距"看得见的地方
- const rr=-Rb*0.70, ch=26, be=S.pitch*Math.PI/180;
- x.save(); x.translate(0,rr); x.rotate(be);
- x.strokeStyle='#0b0d11'; x.lineWidth=3;
- x.beginPath(); x.ellipse(0,0,ch/2,4.5,0,0,2*Math.PI); x.stroke();
- x.fillStyle='#e6e9ef'; x.beginPath(); x.ellipse(0,0,ch/2,4.5,0,0,2*Math.PI); x.fill();
- x.strokeStyle=col; x.lineWidth=2;
- x.beginPath(); x.moveTo(-ch/2,0); x.lineTo(ch/2,0); x.stroke();
- x.restore();
- x.restore();
- }
- /* 轮毂 */
- x.fillStyle='#3a4250'; x.beginPath(); x.arc(hubX,hubY,13,0,2*Math.PI); x.fill();
- x.fillStyle=col; x.beginPath(); x.arc(hubX,hubY,6,0,2*Math.PI); x.fill();
- /* 桨距放大示意: 右下角单叶截面 + 角度弧 */
- const px=W-118, py=H-92;
- x.strokeStyle='#2a3040'; x.beginPath(); x.moveTo(px-52,py); x.lineTo(px+52,py); x.stroke();
- x.fillStyle='#5a6070'; x.font='10px sans-serif'; x.fillText('旋转平面', px-52, py+13);
- x.save(); x.translate(px,py); x.rotate(S.pitch*Math.PI/180);
- x.strokeStyle=col; x.lineWidth=3.5; x.beginPath(); x.moveTo(-46,0); x.lineTo(46,0); x.stroke();
- x.fillStyle=col; x.beginPath(); x.ellipse(0,0,46,7,0,0,2*Math.PI); x.globalAlpha=.28; x.fill(); x.globalAlpha=1;
- x.restore();
- x.strokeStyle=col; x.lineWidth=1.4; x.beginPath();
- x.arc(px,py,30,0,S.pitch*Math.PI/180); x.stroke();
- x.fillStyle=col; x.font='600 15px sans-serif';
- x.fillText('β = '+(MASK?'▪▪':S.pitch.toFixed(1))+'°', px-34, py-42);
- /* 大字读数 */
- const mk=v=>MASK?'▪▪▪':v;
- const rd=[['转速',mk(S.rpm.toFixed(0)),'rpm',S.rpm>(P.overSpd||1e9)*0.97?'#e06c6c':'#e6e9ef'],
- ['转矩',mk(S.tq.toFixed(1)),'kNm','#e0b341'],
- ['功率',mk(S.pwr.toFixed(0)),'kW','#4ec9a0'],
- ['转子',mk((S.rpm/P.ratio).toFixed(2)),'rpm','#8b93a3']];
- rd.forEach((r,k)=>{
- const bx=20, by=H*0.44+k*30;
- x.fillStyle='#8b93a3'; x.font='11px sans-serif'; x.fillText(r[0], bx, by);
- x.fillStyle=r[3]; x.font='600 19px ui-monospace,Menlo,monospace'; x.fillText(r[1], bx+34, by+1);
- x.fillStyle='#5a6070'; x.font='10px sans-serif'; x.fillText(r[2], bx+34+x.measureText(r[1]).width*1.55, by);
- });
- /* 状态角标 */
- x.fillStyle=col; x.font='600 13px sans-serif';
- const lab = S.bp!==null? ('停机程序'+(MASK?'':' BP'+S.bp)+' · 段'+(S.bpPhase+1))
- : S.mode===20? '并网发电' : S.mode===13? '启动 · 开桨' : S.mode===19? '软切入' : '待机';
- x.fillText(lab, W-x.measureText(lab).width-16, 22);
- }
- /* ── 功率流链条 ── */
- function drawFlow(){
- const c=document.getElementById('cv-flow'), x=c.getContext('2d');
- const W=c.width,H=c.height; x.fillStyle='#0b0d11'; x.fillRect(0,0,W,H);
- const Pn = P.ratedKW || Math.max(1, S.pwr); // 无额定值则按当前功率归一, 不写死铭牌值
- const pa = S.pwr/0.94/0.97, stages=[
- {n:'风', v:null, c:'#3d5a80'},
- {n:'气动', v:Math.max(0,pa), c:'#5aa9e6'},
- {n:'齿箱', v:Math.max(0,S.pwr/0.94), c:'#7bb8c9'},
- {n:'发电机', v:Math.max(0,S.pwr/0.97), c:'#e0b341'},
- {n:'变频器', v:Math.max(0,S.pwr), c:'#c99a4e'},
- {n:'电网', v:Math.max(0,S.pwr*(1-(S.curtail||0))), c:'#4ec9a0'},
- ];
- const bw=W/stages.length, maxH=H-46;
- stages.forEach((s,i)=>{
- const cx=i*bw+bw/2;
- if(s.v!==null){
- const h=Math.max(3, Math.min(1,s.v/Pn)*maxH);
- x.fillStyle=s.c; x.globalAlpha=.85;
- x.fillRect(cx-bw*0.3, H-24-h, bw*0.6, h); x.globalAlpha=1;
- x.fillStyle='#e6e9ef'; x.font='600 12px ui-monospace,monospace';
- const lb=s.v.toFixed(0);
- const lb2=MASK?'▪▪▪':lb; x.fillText(lb2, cx-x.measureText(lb2).width/2, H-30-h);
- } else {
- x.fillStyle=s.c; x.font='600 13px sans-serif';
- const lb=(MASK?'▪▪':S.wsInst.toFixed(1))+' m/s';
- x.fillText(lb, cx-x.measureText(lb).width/2, H-24-maxH*0.5);
- }
- x.fillStyle='#8b93a3'; x.font='11px sans-serif';
- x.fillText(s.n, cx-x.measureText(s.n).width/2, H-8);
- if(i<stages.length-1){
- x.strokeStyle='#2a3040'; x.beginPath();
- x.moveTo(cx+bw*0.32,H-24-maxH*0.25); x.lineTo(cx+bw*0.68,H-24-maxH*0.25); x.stroke();
- }
- });
- x.strokeStyle='#232733'; x.beginPath(); x.moveTo(0,H-24); x.lineTo(W,H-24); x.stroke();
- x.fillStyle='#5a6070'; x.font='10px sans-serif'; x.fillText('kW · 柱高对额定归一 (效率取仿真常数)', 6, 13);
- }
- /* ── 转矩-转速 工作点图: 把控制律画出来 ── */
- function drawMap(){
- const c=document.getElementById('cv-map'), x=c.getContext('2d');
- const W=c.width,H=c.height; x.fillStyle='#0b0d11'; x.fillRect(0,0,W,H);
- if(!P.overSpd||!P.tqMax) return;
- const L=52,B=34, rMax=P.overSpd*1.03, tMax=P.tqMax/1000*1.15;
- const sx=r=>L+r/rMax*(W-L-14), sy=q=>H-B-q/tMax*(H-B-16);
- // 网格
- x.strokeStyle='#171b23'; x.lineWidth=1;
- 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(); }
- 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(); }
- // 塔共振禁区带
- if(P.resoDn&&P.resoUp){
- x.fillStyle='#e06c6c1f'; x.fillRect(sx(P.resoDn),16,sx(P.resoUp)-sx(P.resoDn),H-B-16);
- x.strokeStyle='#e06c6c88'; x.setLineDash([4,3]);
- x.strokeRect(sx(P.resoDn),16,sx(P.resoUp)-sx(P.resoDn),H-B-16); x.setLineDash([]);
- x.fillStyle='#e06c6c'; x.font='10px sans-serif'; x.fillText('塔共振禁区', sx(P.resoDn)+3, 28);
- }
- // 额定 / 超速 竖线
- [[P.ratedRpm,'#5aa9e6','额定'],[P.overSpd,'#e06c6c','超速']].forEach(([v,cc,lb])=>{
- x.strokeStyle=cc; x.setLineDash([5,4]); x.beginPath(); x.moveTo(sx(v),16); x.lineTo(sx(v),H-B); x.stroke(); x.setLineDash([]);
- x.fillStyle=cc; x.font='10px sans-serif'; x.fillText(lb, sx(v)-x.measureText(lb).width/2, H-B+13);
- });
- // 控制律曲线 (直接调 torqueLaw, 所以画的就是在跑的那条)
- x.strokeStyle='#4ec9a0'; x.lineWidth=2; x.beginPath();
- 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); }
- x.stroke(); x.lineWidth=1;
- // 轨迹
- if(S.hist.length>1){
- x.strokeStyle='#e0b34155'; x.beginPath();
- S.hist.forEach((h,i)=>{ const px=sx(h.rpm),py=sy(h.tq); i?x.lineTo(px,py):x.moveTo(px,py); });
- x.stroke();
- }
- // 工作点
- const opx=sx(S.rpm), opy=sy(S.tq);
- x.fillStyle=stCol(); x.beginPath(); x.arc(opx,opy,6,0,2*Math.PI); x.fill();
- x.strokeStyle='#0b0d11'; x.lineWidth=2; x.beginPath(); x.arc(opx,opy,6,0,2*Math.PI); x.stroke(); x.lineWidth=1;
- x.fillStyle='#8b93a3'; x.font='10px sans-serif';
- x.fillText('转矩 kNm', 6, 22); x.fillText('发电机转速 rpm', W-96, H-6);
- if(!MASK){ x.fillText('0', L-10, H-B+4);
- x.fillText((P.tqMax/1000).toFixed(0), L-26, sy(P.tqMax/1000)+4); }
- }
- function drawState(){
- const c=document.getElementById('cv-state'), x=c.getContext('2d');
- const W=c.width,H=c.height; x.fillStyle='#0b0d11'; x.fillRect(0,0,W,H);
- const groups=[{l:'刹车 0-6',ids:[0,1,2,3,4,5,6],c:'#e06c6c'},
- {l:'待机/自检',ids:[7,8,9,10,11,12,26],c:'#8b93a3'},
- {l:'启动',ids:[13],c:'#e0b341'},
- {l:'DFIG 死块',ids:[14,15,16,18],c:'#4a4f5c'},
- {l:'CAGE 实机',ids:[19,20,21,23],c:'#4ec9a0'},
- {l:'互切',ids:[24,25],c:'#a48ac8'}];
- let gx=8; x.font='9px sans-serif';
- groups.forEach(g=>{
- x.fillStyle=g.c; x.fillText(g.l,gx,11);
- g.ids.forEach((id,i)=>{
- const bx=gx+i*15, by=17;
- x.fillStyle = id===S.mode? g.c : g.c+'2e';
- x.fillRect(bx,by,13,13);
- x.fillStyle = id===S.mode? '#08121c':'#9aa2b1'; x.font='8px sans-serif';
- x.fillText(String(id), bx+(id<10?4:1), by+9); x.font='9px sans-serif';
- });
- gx += Math.max(g.ids.length*15, 56)+14;
- });
- x.fillStyle='#5a6070'; x.font='10px sans-serif';
- x.fillText('实测启动 13→19→20,不经 14-18 ⇒ DFIG 族恒不进入(软件预留)', 8, H-8);
- }
- function updateReadouts(){
- document.getElementById('v-ws').textContent=document.getElementById('in-ws').value;
- document.getElementById('v-ti').textContent=document.getElementById('in-ti').value;
- const M=F.operation_modes.modes;
- document.getElementById('v-mode').textContent =
- S.mode+' '+(M[S.mode]||'—')+(S.bp!==null?(' · 停机段'+(S.bpPhase+1)):'');
- }
- /* ── 机制状态: 紧凑 chip, 点开看详情 ── */
- function updateChips(){
- const tz=tezGate(S.rpm);
- const C=[
- ['M1 塔共振转速禁区', tz.inZone?'on':'off', `PLC 侧 ${fmt(P.resoDn,0)}–${fmt(P.resoUp,0)} rpm 生效 · RTW 侧 ${fmt(P.tezLo,0)}–${fmt(P.tezHi,0)} 未使能`],
- ['M2 塔阻尼 fore-aft', S.faActive?'on':'off', `走变桨 · 功率滞环 ${fmt(P.faOn,2)}/${fmt(P.faOff,2)}`],
- ['M2b 塔阻尼 side-side','dis',`走转矩 · Enable=${fmt(P.ssEnable,0)}`],
- ['M3 传动链扭振+陷波','on','定标未锚 · 只用比值与排序'],
- ['M4 最优增益查表',(S.mode===20&&S.rpm<P.ratedRpm)?'on':'off',`${MASK?'▪▪':(P.optGainX?P.optGainX.length:'—')} 点 · 自适应层出厂未学习`],
- ['M5 变桨 PI 增益调度',(S.pitch>0.2&&S.mode===20)?'on':'off',`Kp 随 β 线性 · β∈[${fmt(P.pitLo,0)},${fmt(P.pitHi,0)}]°`],
- ['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)}`],
- ['M7 刹车程序三套编号','on', pz('代码 7 档 / 码表 15 / 交付阶梯 24 —— 勿混')],
- ['M8 LVRT', S.lvrt>0?'on':'off','CAGE/DFIG 双分支 · 无 crowbar/chopper'],
- ['M9 变桨后备电池', S.batt?'on':'off','蓄能器仅自检层 · 监视≠可驱动'],
- ['M10 独立变桨 ALC','dis','4 接近探头 @45/135/225/315°'],
- ['M11 叶片摆振滤波', P.bladeEdge?'on':'off','版本增量 48 项之一'],
- ['M12 限电损失表', S.curtail>0?'on':'off', MASK?'功率损失查表(已隐)':'CON_CURTAIL_ET_LOSS / PWR'],
- ['M13 DFIG 分支','dead','符号层与 CAGE 逐条对称 ⇒ 活死只能靠运行数据判'],
- ];
- document.getElementById('chips').innerHTML = C.map(([n,st,d])=>
- `<div class="chip ${st}"><span class="dot"></span><span class="cn">${n}</span><span class="cd">${d}</span></div>`).join('');
- document.querySelectorAll('#chips .cd').forEach(e=>maskTextNodes(e));
- const noteEl=document.getElementById('note-map');
- if(noteEl) noteEl.innerHTML='绿线 = 正在跑的转矩律(直接调 <code>torqueLaw</code> 画出,非另画一条)· 黄迹 = 近 3 分钟工作点轨迹 · 红带 = 塔共振禁区';
- }
- /* ══════════════ 静态面板 ══════════════ */
- function buildStatic(){
- const I=F.identity.identity;
- const BL=F.baselines.baselines.shutdown_programs_measured;
- document.getElementById('hdr-id').innerHTML =
- I.platform.model+' · '+I.platform.rating+' · '+txt(I.platform.site)+
- ' <span class="badge b-tpl">参数=模板值</span>'+
- (MASK?' <span class="badge b-off">关键细节已隐去</span>':'');
- // 三系统
- const sub=document.getElementById('subsys'); if(sub) sub.innerHTML = `
- <h3>主控 <span class="badge b-tpl">模板值</span></h3>
- <div class="row"><span class="k">平台</span><span class="v">${I.controller.plc}</span></div>
- <div class="row"><span class="k">核控</span><span class="v">${I.controller.core}</span></div>
- <div class="row"><span class="k">额定转速</span><span class="v">${fmt(P.ratedRpm,0,'rpm')}</span></div>
- <h3>变桨 <span class="badge b-tpl">模板值</span></h3>
- <div class="row"><span class="k">型式</span><span class="v">${I.pitch.type}</span></div>
- <div class="row"><span class="k">总线</span><span class="v">${I.pitch.comm}</span></div>
- <div class="row"><span class="k">速率上限</span><span class="v">${fmt(P.rMax,2,'°/s')}</span></div>
- <div class="row"><span class="k">电池后备限幅</span><span class="v">${fmt(P.rBatt,2,'°/s')}</span></div>
- <h3>变频 <span class="badge b-tpl">模板值</span></h3>
- <div class="row"><span class="k">型号</span><span class="v">${I.converter.model}</span></div>
- <div class="row"><span class="k">固件</span><span class="v">${I.converter.fw}</span></div>
- <div class="row"><span class="k">LVRT 转矩限幅系数</span><span class="v">${fmt(P.lvrtClamp,3)}</span></div>
- <div class="note">★ 变桨后备驱动路径在代码里<b>只有电池一条</b>;蓄能器仅出现在自检状态码层(<code>SC_SELFTEST_PitchAccumulatorStatusError1..3</code>),<b>无经蓄能器驱动变桨的 POU 或分支</b> —— 与 EN15 的 <code>BNOPITCHDRIVEVIAACCUMULATOR</code> 不同构。</div>
- `;
- // 证据等级
- document.getElementById('evid').innerHTML = `
- <div class="row"><span class="k">包内自标</span><span class="v"><span class="badge b-tpl">模板值</span></span></div>
- <div class="note" style="border-top:none;margin-top:2px">
- <b>本包无一条够得上「实机回读」。</b>作分组依据/设计意图合格,<b>作阈值锚不合格</b>。
- 页内不出现「真值」二字。
- </div>
- <h3>可作行为锚的(调机期实测)</h3>
- <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>
- <div class="row"><span class="k">BP51 卸载期</span><span class="v"><span class="badge b-ins">非恒速率</span></span></div>
- <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>
- <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>
- <div class="note">
- ${MASK?'<b>关键数值已隐去</b>(下列订正保留结论与方向,数字以 ▪▪ 代)<br>':''}
- ⚠ <b>三条订正随页走,不可略</b>:<br>
- ① 原记「卸载期 1.000 = <code>Con_PitchSlowRate</code>」在 repo 内保留的 K2 切片上<b>不可复现</b>(该事件内无 1.0 段,均值 0.831 且全程在变)。原结论建立在 3 个事件上,仅 1 个被复核 ⇒ <b>该腿降 INSUFFICIENT</b>,仅 <code>NormalStopRate</code> 腿经行为验证。<br>
- ② <b>「分段」不等于「每段都是恒速率」</b>。该过程实为 速率受限段(2.000, 参数 binding) + 一个前置的非速率受限段(疑转矩/功率受限,机制 INSUFFICIENT)。<br>
- ③ 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>
- ④ 频率类参数(<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>
- ⑤ <code>TowerFrequency</code> 的十倍频<b>已锚</b>:PLC 转速带 × 实测传动比 → 3P 上限对上 RTW 塔频,差 0.7%(跨参数体系闭合)。但<b>中心</b>差 3~4%,两套禁区各自反推的隐含塔频彼此只差 0.7% 却与参数差 3~4% ⇒ <b>闭合成立、但闭合到的可能不是该参数</b>,未决。
- </div>
- <h3>边界</h3>
- <div class="note" style="border-top:none">
- · 气动模型(Cp-λ-β)与折算惯量 J 是<b>仿真常数</b>,非 OEM 参数,不可外引。<br>
- · 调机期基线 = 轻载/无气动载,<b>只迁形态与相对判据,不迁绝对数值</b>;覆盖面 = 单台 K19/K2、单叶、n=43、2014-15。<br>
- · 本页所有参数 <b>运行时从同目录 JSON 读</b>,源码零内嵌。演示/上云开「公开模式」。
- </div>
- `;
- maskTextNodes(document.getElementById('evid'));
- maskTextNodes(document.getElementById('subsys'));
- }
- function updateMechs(){
- const on=(b)=>b?'active':'disabled';
- const tz=tezGate(S.rpm);
- const M=[
- {t:'M1 塔共振转速禁区(两套并存)', cls: tz.inZone?'active':'disabled',
- d:`PLC <code>PAR_rTowerResoGenSpeedDown/Up</code> = ${fmt(P.resoDn,0)} / ${fmt(P.resoUp,0)} rpm <span class="badge b-tpl">生效</span><br>
- RTW <code>TEZLowerRpm/TEZRpm/TEZUpperRpm</code> = ${fmt(P.tezLo,0)} / ${fmt(P.tezMid,0)} / ${fmt(P.tezHi,0)} rpm,
- <code>TEZ_Enable</code>=${fmt(P.tezEnable,0)} <span class="badge b-off">未使能</span><br>
- 当前 ${tz.inZone? '<b>在禁区内</b>,dwell '+S.tezTimer.toFixed(1)+'s':'不在禁区'}`},
- {t:'M2 塔架阻尼(双通道)', cls: S.faActive?'active':'disabled',
- d:`fore-aft 走<b>变桨</b>:<code>CON_PITCH_TWR_NUM1/2 DEN1/2 G</code> 二阶离散传函;功率滞环 投入 ${fmt(P.faOn,2)} / 退出 ${fmt(P.faOff,2)}<br>
- side-side 走<b>转矩</b>:<code>TwrSideSideTorq_Kp/Ti</code>,<code>Enable</code>=${fmt(P.ssEnable,0)} <span class="badge b-off">未使能</span><br>
- <code>TowerFrequency</code> = ${fmt(P.fTower,3,'Hz')} <span class="badge b-meas">十倍频已锚</span>`},
- {t:'M3 传动链扭振阻尼 + 陷波', cls:'active',
- d:`<code>Con_DRVDamper_{Freq,Freq2,K,K2}</code> 双模态 · <code>Con_NotchFilter_Freq</code> · <code>DTFrequency</code><br>
- <span class="badge b-ins">定标未锚</span> 停机录波峰位随转速漂移 ⇒ 阶次内容非结构线,<b>只用比值与排序</b>`},
- {t:'M4 额定以下 最优增益查表', cls: (S.mode===20&&S.rpm<P.ratedRpm)?'active':'disabled',
- d:`<code>CON_GENSPEED_OPTGAIN_X/Y</code> ${P.optGainX?P.optGainX.length:'—'} 点<br>
- 自适应层 <code>NV_compressed_RTW</code>(OptimalGainCorr/PitchOffset/Power 各 30 点风速格)<b>出厂全 1/全 0 = 未学习</b>`},
- {t:'M5 额定以上 PI 增益按桨角线性调度', cls: (S.pitch>0.2&&S.mode===20)?'active':'disabled',
- d:`Kp = ${fmt(P.kpInt,1)} + ${fmt(P.kpSlope,3)}·β,Ti = ${fmt(P.tiInt,1)} + ${fmt(P.tiSlope,3)}·β<br>
- 调度区间 β ∈ [${fmt(P.pitLo,0)}, ${fmt(P.pitHi,0)}]°`},
- {t:'M6 变桨速率分级', cls: S.bp!==null?'active':'disabled',
- 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>
- <span class="badge b-bad">Battery 与 Max 同值</span> ⇒ 参数独立存在但<b>取值不独立</b>,后备态不降速率`},
- {t:'M7 刹车程序 —— 三套编号并存,勿混', cls:'active',
- d:`代码 POU 档位 <b>7</b> 个(wbp 符号)· 码表设计列 <b>15</b> 个取值 · 交付阶梯 <b>24</b> 个取值<br>
- <b>代码有 ∧ 设计列无 = 空集</b>(SC-1 无「代码里有、码表里无」的档;EN15 的 198/30 无码属平台差异)<br>
- ⚠ 用错列会把 51/100 也算成「代码有码表无」,而它们分别有 316 / 95 条码`},
- {t:'M8 LVRT(双拓扑分支)', cls: S.lvrt>0?'active':'disabled',
- d:`主控侧仅 <code>LVRT_TorqLimitClampCoeff</code> = ${fmt(P.lvrtClamp,3)};<b>crowbar / chopper 包内零命中</b><br>
- 状态机 <code>CAGE_LVRT</code>(21) 与 <code>DFIG_LVRT</code>(16) <b>并列存在</b> ⇒ 两套穿越逻辑都在软件里`},
- {t:'M9 变桨后备(电池路径)', cls: S.batt?'active':'disabled',
- d:`<code>Con_PitchBatteryMaxRate</code>;每桨 <code>AI_BLADE{1,2,3}_BATTERYVOLTAGE</code> / <code>TEMPBATTERYBOX</code>(BATTERY 词根 225 条)<br>
- ACCUMULATOR 53 条中<b>仅 3 条与变桨相关且全是自检码</b> ⇒ 监视 ≠ 可驱动`},
- {t:'M10 独立变桨 ALC', cls:'disabled',
- d:`<code>ALCPitch_{Kp,Ti,tao,AzimOffset,MaxOffset}</code> · <code>IndPitch_MaxOffset</code> · 轮毂载荷 4 接近探头 @45/135/225/315°`},
- {t:'M11 叶片摆振滤波', cls: P.bladeEdge?'active':'disabled',
- d:`<code>CON_BladeEdge_{Num,Den,Mag,DP}</code> · <code>EnableBladeEdgeMonitor</code>=${fmt(P.bladeEdge,0)}<br>
- 属新旧版本增量 48 项之一(334 → 379 参数)`},
- {t:'M12 限电功率损失表', cls: S.curtail>0?'active':'disabled',
- d:`<code>CON_CURTAIL_ET_LOSS</code> / <code>CON_CURTAIL_PWR</code>(同属增量 48 项)`},
- {t:'M13 DFIG 分支 —— 软件预留,恒不进入', cls:'dead',
- d:`状态 14-18 + <code>Cage_DFIG_Enable</code>=${fmt(P.cageDfig,0)} + wbp 符号 11 条(与 CAGE 族 11 条<b>逐条一一对应</b>)<br>
- <b>符号层完全无法区分死块与活块</b>:前缀/后缀/PRG_ 块/三联参数全部对称 ⇒ 活死判别只能靠<b>运行数据的模式列直方图</b>`},
- ];
- document.getElementById('mechs').innerHTML = M.map(m=>
- `<div class="mech ${m.cls}"><div class="t">${m.t}</div><div class="d">${m.d}</div></div>`).join('');
- }
- /* ══════════════ 交互 ══════════════ */
- document.getElementById('btn-run').onclick = e=>{
- running=!running; e.target.classList.toggle('on',running);
- e.target.textContent = running?'⏸ 暂停':'▶ 运行';
- };
- document.getElementById('btn-reset').onclick = ()=>{ reset(); draw(); };
- document.getElementById('btn-mask').onclick = e=>{
- MASK=!MASK; e.target.classList.toggle('on',MASK);
- e.target.textContent = MASK?'🔒 已隐去关键细节':'🔓 内部模式(显全部)';
- buildStatic(); draw();
- };
- document.querySelectorAll('[data-bp]').forEach(b=> b.onclick=()=>{ trigBP(+b.dataset.bp); });
- document.querySelectorAll('[data-sc]').forEach(b=> b.onclick=()=>{
- const s=b.dataset.sc, ws=document.getElementById('in-ws');
- if(s==='gust'){ S.gust=8; soe('warn','注入阵风 +8 m/s'); }
- if(s==='ramp'){ ws.value=16; soe('info','风速爬坡至 16 m/s'); }
- if(s==='lull'){ ws.value=2.5; soe('info','风速跌至 2.5 m/s'); }
- });
- document.querySelectorAll('[data-inj]').forEach(b=> b.onclick=()=>{
- const k=b.dataset.inj;
- if(k==='lvrt'){ S.lvrt=1.5; soe('warn','电网跌落 · 转矩限幅系数投入'); }
- if(k==='battery'){ S.batt=!S.batt; soe('warn','变桨后备电池 '+(S.batt?'投入':'退出')); }
- if(k==='curtail'){ S.curtail = S.curtail>0?0:0.5; soe('warn','限电 '+(S.curtail>0?'50%':'解除')); }
- });
- function loop(ts){
- if(running){ for(let i=0;i<4;i++) step(); draw(); }
- requestAnimationFrame(loop);
- }
- boot().then(()=>requestAnimationFrame(loop));
- </script>
|