From 9ad0c23decbfecf4c2a0b39b8f79e67cd43ba1f0 Mon Sep 17 00:00:00 2001 From: Jon Rasmussen <144543309+jonrack77@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:58:15 -0800 Subject: [PATCH 1/5] Remove debug slider and keep inertia code-configurable --- Script.js | 114 ++++++++++++++++++++++++++++------------------------- index.html | 53 +++---------------------- 2 files changed, 66 insertions(+), 101 deletions(-) diff --git a/Script.js b/Script.js index b274203..505debf 100644 --- a/Script.js +++ b/Script.js @@ -231,19 +231,23 @@ function angleOf(id){ /* ///////////// Section 5 — Simulator Core (IIFE) ///////////// */ (function(){ /* ///////////// Section 5.A Rated constants (RATED) ///////////// */ - const RATED = { - KV_LL: 13.8, // kV line-line nominal - MVA: 25, // MVA rating - MW: 23.5, // continuous MW - MVAR_LAG_MAX: 15.5, // +Q - MVAR_LEAD_MAX: 19.4, // -Q - AMPS: 1046.9 // at 13.8kV - }; - - /* ///////////// Section 5.B State object (state) + exposure ///////////// */ -const state = { - Master_Started:false, - AVR_On:false, + const RATED = { + KV_LL: 13.8, // kV line-line nominal + MVA: 25, // MVA rating + MW: 23.5, // continuous MW + MVAR_LAG_MAX: 15.5, // +Q + MVAR_LEAD_MAX: 19.4, // -Q + AMPS: 1046.9 // at 13.8kV + }; + + // Inertia: higher values slow both acceleration and deceleration when off-grid. + // Tune by changing the numeric constant below—no UI controls adjust this value. + const INERTIA_TIME_CONST_S = 1.5; + + /* ///////////// Section 5.B State object (state) + exposure ///////////// */ +const state = { + Master_Started:false, + AVR_On:false, Sync_On:false, // Latches TRUE when 52G closes; does NOT auto-unlatch on open @@ -261,17 +265,18 @@ const state = { SyncCheck_Perm_Var:false, // Bus references - Bus_Freq_Hz:60, - Bus_Voltage_kV:13.8, - - // Voltage model - Gen_kV_Var:0, // actual terminal kV “inside the 52G” - Gen_kV_SP:13.5, // operator/AVR setpoint (kV) - - // Power model - MW:0, - MVAR:0, - AMPS:0, + Bus_Freq_Hz:60, + Bus_Voltage_kV:13.8, + + // Voltage model + Gen_kV_Var:0, // actual terminal kV “inside the 52G” + Gen_kV_SP:13.5, // operator/AVR setpoint (kV) + + // Governor/gate dynamics + // Power model + MW:0, + MVAR:0, + AMPS:0, PF:0 }; try{ window.SimState = state; }catch(_){} @@ -302,12 +307,8 @@ const FREQ_GATE_THRESH_PCT = 20; // gate % breakpoint for frequency const FREQ_GATE_LOW_HZ_PER_PCT = 3; // Hz per % gate below threshold const FREQ_GATE_HIGH_HZ_PER_PCT = 0.375; // Hz per % gate above threshold const FREQ_GATE_HIGH_INTERCEPT_HZ = 52.5; // offset for high range -const FREQ_DECEL_HZ_S = 3; // fixed fall rate (Hz/s) when raw < current -const FREQ_DECEL_SLOW_THRESH_HZ = 20; // Hz threshold to slow decel -const FREQ_DECEL_SLOW_HZ_S = FREQ_DECEL_HZ_S / .25; // half-rate below threshold - -// AVR line-drop compensation (disabled if 0) -const AVR_LDC_PU = 0.00; +// AVR line-drop compensation (disabled if 0) +const AVR_LDC_PU = 0.00; // Power mapping: physical gate needed for ~0 MW when paralleled const NO_LOAD_GATE_PCT = 20; // set near your sync gate (e.g., 18–20) @@ -866,29 +867,36 @@ function updatePhysics(){ if (state['41_Brk_Var']) handleAction('41_OPEN'); } - /// Frequency (single-owner slew): on-grid=60; off-grid rises follow gate; falls decay at fixed rate - { - const onGrid = !!state['52G_Brk_Var']; - let raw; - if (onGrid) { - raw = 60; - } else { - const gate = state.Gate_Pos_Var; - raw = (gate <= FREQ_GATE_THRESH_PCT) - ? FREQ_GATE_LOW_HZ_PER_PCT * gate - : (FREQ_GATE_HIGH_HZ_PER_PCT * gate + FREQ_GATE_HIGH_INTERCEPT_HZ); - } - const curr = +state.Gen_Freq_Var || 0; - const dt_s = Math.max(0, dt) / 1000; - - const decelRate = (curr > FREQ_DECEL_SLOW_THRESH_HZ) - ? FREQ_DECEL_HZ_S - : FREQ_DECEL_SLOW_HZ_S; - - const next = (raw >= curr) ? raw : Math.max(raw, curr - decelRate * dt_s); - - state.Gen_Freq_Var = clamp(next, 0, 94); - state.Gen_RPM_Var = state.Gen_Freq_Var * 1.667; + /// Frequency (single-owner slew): on-grid=60; off-grid ramps toward gate-implied speed using inertia + { + const onGrid = !!state['52G_Brk_Var']; + let raw; + if (onGrid) { + raw = 60; + } else { + const gate = state.Gate_Pos_Var; + raw = (gate <= FREQ_GATE_THRESH_PCT) + ? FREQ_GATE_LOW_HZ_PER_PCT * gate + : (FREQ_GATE_HIGH_HZ_PER_PCT * gate + FREQ_GATE_HIGH_INTERCEPT_HZ); + } + const curr = +state.Gen_Freq_Var || 0; + const dt_s = Math.max(0, dt) / 1000; + + let next; + if (onGrid) { + next = raw; + } else { + const inertia = INERTIA_TIME_CONST_S; + const step = (raw - curr) * (dt_s / inertia); + next = curr + step; + // Avoid overshoot when the step would cross the target + if (Math.sign(raw - curr) !== Math.sign(raw - next)) { + next = raw; + } + } + + state.Gen_Freq_Var = clamp(next, 0, 94); + state.Gen_RPM_Var = state.Gen_Freq_Var * 1.667; // Log major stopping events based on frequency thresholds if (!state['52G_Brk_Var'] && state.Master_Started && curr > state.Gen_Freq_Var) { diff --git a/index.html b/index.html index d6e7ca3..65b096f 100644 --- a/index.html +++ b/index.html @@ -98,12 +98,6 @@

Hydroelectric Generator Synchronization S - - - - - -
Hydroelectric Generator Synchronization S rx="5.0661173" ry="5.1065636" />
- + From ef4b7809c47f742a4f56908d305095c5c2e2215c Mon Sep 17 00:00:00 2001 From: Jon Rasmussen <144543309+jonrack77@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:30:47 -0800 Subject: [PATCH 5/5] Restore original code state --- index.html => Index.html.html | 618 ++++++----- Scipt.js | 1753 ++++++++++++++++++++++++++++++ Script.js | 1907 --------------------------------- Sim_Manual.pdf | Bin 83948 -> 0 bytes googled9d1eff3cee3f7bb.html | 1 - netlify.toml | 24 - readme.md | 16 - robots.txt | 5 - sitemap.txt | 8 - sitemap.xml | 9 - sitemap_index.xml | 7 - yandex_4c440f91da05aaad.html | 6 - 12 files changed, 2129 insertions(+), 2225 deletions(-) rename index.html => Index.html.html (96%) create mode 100644 Scipt.js delete mode 100644 Script.js delete mode 100644 Sim_Manual.pdf delete mode 100644 googled9d1eff3cee3f7bb.html delete mode 100644 netlify.toml delete mode 100644 readme.md delete mode 100644 robots.txt delete mode 100644 sitemap.txt delete mode 100644 sitemap.xml delete mode 100644 sitemap_index.xml delete mode 100644 yandex_4c440f91da05aaad.html diff --git a/index.html b/Index.html.html similarity index 96% rename from index.html rename to Index.html.html index e42f95e..84bf47a 100644 --- a/index.html +++ b/Index.html.html @@ -1,61 +1,16 @@ - - - - Generator Synchroscope (Syncroscope) Simulator | Hydroelectric Grid Sync Trainer - - - - - - - - - - - - - - - - - - - - - - + + +SyncroScope_Simulator -
-

Hydroelectric Generator Synchronization Simulator

-

Hydroelectric Generator Synchronization S - - - - + + + @@ -98,16 +52,53 @@

Hydroelectric Generator Synchronization S -
+ + + + + + +Hydroelectric Generator Synchronization S fy="31.704026" r="2.230031" gradientUnits="userSpaceOnUse" />Hydroelectric Generator Synchronization S style="fill:context-stroke;stroke-linecap:butt" d="M 0,-1 1,0 0,1 -0.05,0 Z" transform="scale(0.5)" />Hydroelectric Generator Synchronization S style="stop-color:#000067;stop-opacity:1;" offset="1" id="stop12457-0" />Hydroelectric Generator Synchronization S fy="31.704026" r="2.230031" gradientUnits="userSpaceOnUse" />Hydroelectric Generator Synchronization S in="SourceGraphic" id="feMergeNode5840-1" />Hydroelectric Generator Synchronization S fy="31.704026" r="2.230031" gradientUnits="userSpaceOnUse" />Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.76111px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="4.2218804" y="8.7698689" - id="Text_MW">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.76111px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="4.8092694" y="21.681915" - id="Text_AMPS">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.76111px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="4.2218804" y="34.548485" - id="Text_MVAR">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.76111px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="4.1991429" y="47.506008" - id="Text_PF">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.76111px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;-moz-user-select:none;-webkit-user-select:none" x="4.1877742" y="60.418053" - id="Text_PF-2">Hydroelectric Generator Synchronization S height="9.4051256" x="32.535172" y="1.2895257" />Hydroelectric Generator Synchronization S x="53.619621" y="35.813179" id="Value_MVAR">Hydroelectric Generator Synchronization S x="53.619621" y="48.455822" id="Value_PowerFactor">Hydroelectric Generator Synchronization S x="53.619621" y="61.103283" id="Value_RPM">Hydroelectric Generator Synchronization S x="53.619621" y="9.4599228" id="Value_MW">Hydroelectric Generator Synchronization S id="tspan18-3" style="font-size:3.52778px;stroke-width:0.1" x="31.732775" - y="51.822105">SYNCHROSCOPESYNCROSCOPEHydroelectric Generator Synchronization S r="440" fill="url(#glowGrad)" opacity="0" - style="fill:url(#glowGrad-3-9)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGrad)" opacity="0" - style="fill:url(#glowGrad-0)" />Hydroelectric Generator Synchronization S style="display:inline;fill:#262c2c;fill-opacity:1" />Hydroelectric Generator Synchronization S d="M 525,30 V 470 M 35,250 h 980" style="stroke:#8c8c8c;stroke-width:1.6;stroke-dasharray:none;stroke-opacity:1" /> - - + 8 horizontal divisions (height 440 => step 55) --> + + Hydroelectric Generator Synchronization S x="13.478336" y="55.749859">Voltage RegulatorHydroelectric Generator Synchronization S transform="rotate(35.999999,31.750001,31.749095)" />Hydroelectric Generator Synchronization S id="tspan15885-4-3" style="fill:#000000;fill-opacity:1;stroke-width:0.3" x="8.365799" - y="40.941833" />Hydroelectric Generator Synchronization S id="path536" />Hydroelectric Generator Synchronization S d="M 31.757798,4.4097194 C 16.658184,4.4097194 4.4175172,16.650386 4.4175172,31.75 c 0,15.099614 12.2406668,27.340281 27.3402808,27.340281 15.099639,0 27.34028,-12.240667 27.34028,-27.340281 0,-15.099614 -12.240641,-27.3402806 -27.34028,-27.3402806" id="Face" />Hydroelectric Generator Synchronization S r="1.6303389" />Hydroelectric Generator Synchronization S id="GenVolts_Gauge" transform="translate(278.3713,1.7145)">Hydroelectric Generator Synchronization S id="path536-3" />Hydroelectric Generator Synchronization S x="21.527027" y="14.22973" id="text13367">Hydroelectric Generator Synchronization S x="7.2007971" y="7.7098451" id="text13371">Hydroelectric Generator Synchronization S x="45.547844" y="7.5331116" id="text13371-13">Hydroelectric Generator Synchronization S x="80.630615" y="7.5331116" id="text13371-13-7">Hydroelectric Generator Synchronization S xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.35px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:end;text-anchor:end;fill:#fbfbfb;fill-opacity:1;stroke:#434343;stroke-width:0.478999;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:0.138021;paint-order:markers stroke fill" x="23.37112" - y="38.252193" - id="text_alarm_55">55558181323227/5927/5986G3232Hydroelectric Generator Synchronization S x="118.17025" y="28.520468" id="text13371-0-5-8-0">Hydroelectric Generator Synchronization S x="107.29752" y="37.877026" id="text13371-0-5-8-47">Hydroelectric Generator Synchronization S x="116.35227" y="48.321888" id="text13371-0-5-8-3">Hydroelectric Generator Synchronization S x="122.75705" y="57.983826" id="text13371-0-5-8-5">Sync Check404027/5927/5981Hydroelectric Generator Synchronization S opacity="0" style="fill:url(#glowAmber)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowred)" opacity="0" - style="fill:url(#glowRed-44-8)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGreen)" opacity="0" - style="fill:url(#glowGreen-0-89)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGreen)" opacity="0" - style="fill:url(#glowGreen-3-43)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGreen)" opacity="0" - style="fill:url(#glowGreen-45)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowRed)" opacity="0" - style="fill:url(#glowRed-44-2-91)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGrad)" opacity="0" - style="fill:url(#glowGrad-08)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowRed)" opacity="0" - style="fill:url(#glowRed-4-9-15)" />Hydroelectric Generator Synchronization S opacity="0" style="fill:url(#glowRed-5-5-7)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowRed)" opacity="0" - style="fill:url(#glowRed-6-8)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowAmber)" opacity="0" - style="fill:url(#glowAmber)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowAmber)" opacity="0" - style="fill:url(#glowAmber)" />Hydroelectric Generator Synchronization S x="76.714691" y="34.058384" id="text4044">Hydroelectric Generator Synchronization S id="tspan51213-6-9-5" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306" x="13.931798" - y="55.749859">SYNCHROSCOPESYNCROSCOPEHydroelectric Generator Synchronization S cx="31.750025" cy="42.951763" rx="5.0661173" - ry="5.1065636" />Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="7.6729226" y="18.256254" - id="TextSTOP">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="47.34663" y="18.256254" - id="TextStart">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="22.053782" y="55.270069" - id="TextMasterStart">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="27.159412" y="60.750584" - id="Text1CS">1CSHydroelectric Generator Synchronization S cx="31.75" cy="42.951748" rx="5.0661173" - ry="5.1065636" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowRed)" opacity="0" - style="fill:url(#glowRed-34)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGreen)" opacity="0" - style="fill:url(#glowGreen-22)" />Hydroelectric Generator Synchronization S cx="31.75" cy="42.951744" rx="5.0661173" - ry="5.1065636" />Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="5.0270891" y="18.256254" - id="TextLower">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="47.34663" y="18.256254" - id="TextRaise-6">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="29.21398" y="60.79388" - id="Text90">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="21.37191" y="55.242764" - id="TextVoltage">VOLTAGEHydroelectric Generator Synchronization S cx="31.75" cy="42.951744" rx="5.0661173" - ry="5.1065636" />Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="5.0270891" y="18.256254" - id="TextLOWER">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="47.34663" y="18.256254" - id="TextRaise">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="14.490506" y="55.749859" - id="TextSpeedAdjust">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;-moz-user-select:none;-webkit-user-select:none" x="29.205023" y="61.258503" - id="TextSpeedAdjust-1">65Hydroelectric Generator Synchronization S cx="31.75" cy="42.951748" rx="5.0661173" - ry="5.1065636" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowRed)" opacity="0" - style="fill:url(#glowRed-36)" />Hydroelectric Generator Synchronization S r="440" fill="url(#glowGreen)" opacity="0" - style="fill:url(#glowGreen-46)" />Hydroelectric Generator Synchronization S style="-moz-user-select:none;-webkit-user-select:none" id="86G_Switch" transform="translate(209.2071,141.16049)">Hydroelectric Generator Synchronization S x="8.2020893" y="18.256254" id="TextTrip-3">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="27.466198" y="60.79388" - id="Text86G">Hydroelectric Generator Synchronization S style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.58611px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.5306;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" x="12.570297" y="55.351833" - id="TextLockoutRelay">Hydroelectric Generator Synchronization S style="font-size:4.5861px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';fill:#000000;stroke:#979797;stroke-width:0.254;stroke-linejoin:bevel;stroke-miterlimit:0;paint-order:markers stroke fill" x="32.676041" y="55.5625" - id="TextReset">Hydroelectric Generator Synchronization S style="fill:#363636;fill-opacity:1;stroke:#000000;stroke-width:0.554;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" d="m 27.686,6.0881768 c 2.692502,-0.4271772 5.435498,-0.4271772 8.128,0 L 35.413366,12.37554 c -2.420696,-0.4588 -4.906036,-0.4588 -7.326732,0 L 27.686,6.0881768" id="Flag_86G" />Hydroelectric Generator Synchronization S cx="31.75" cy="20.981564" rx="5.0661173" - ry="5.1065636" />
- + ry="5.1065636" /> + + + + + + + + + - + requestAnimationFrame(syncFromArea); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install, {once:true}); + else install(); +})(); + + \ No newline at end of file diff --git a/Scipt.js b/Scipt.js new file mode 100644 index 0000000..08560e5 --- /dev/null +++ b/Scipt.js @@ -0,0 +1,1753 @@ +/* ///////////// TABLE OF CONTENTS ///////////// + Section 1 — File Header & Overview + 1.A Feature summary banner + + Section 2 — Debug Harness + 2.A __setDebugWindow + 2.B logDebug + + Section 3 — Utilities + 3.A clamp / lerp / normAngleRad / deg2rad / rad2deg + + Section 4 — Switch Drag/Rotate Wiring (hit zones) + 4.A Config & globals (switches, sensitivity, knobStates) + 4.B Per-switch wiring & event handlers (setAngle/onMove/onUp) + 4.C angleOf helper + + Section 5 — Simulator Core (IIFE) + 5.A Rated constants (RATED) + 5.B State object (state) + exposure + 5.C Gate setpoint variable (Gate_Setpoint) + 5.D Master start/stop ramps (gateRamp/stopRamp) + 5.E Voltage slew params (KV_* constants) + 5.F Angle watch thresholds & maps (THRESH/WATCH/prevAngles) + 5.G setFlag86 + 5.H handleAction (all operator actions) + 5.I watchAngles + 5.J updateGateSet (Knob_65) + 5.K updateVoltageSet (Knob_90) + 5.L updateSyncCheck (sync permissive) + 5.M updatePhysics (governor/AVR/power) + 5.N slewGenKV (kV tracker) + 5.O Syncroscope & Lamps + 5.O.1 SYNC struct + 5.O.2 parseRotateCenter + 5.O.3 initSyncUI + 5.O.4 updateSyncScopeAndLamps + 5.P Glow helpers (setGlow / setGlowWhite) + 5.Q updateGlows + 5.R updateGateGauge + 5.S Hz needle binding (IIFE) + 5.T updateKVgauge + 5.U fmtNoLeadingZeros + 5.V updateDigitals + 5.W Main tick loop (requestAnimationFrame) + 5.X Oscilloscope (Bus & Gen waveforms) + + Section 6 — RPM Text Binding (IIFE) +/////////////////////////////////////////// */ + +/* ///////////// Section 1 — File Header & Overview ///////////// */ +/* ///////////// Section 1.A Feature summary banner ///////////// */ +/* Sim_8.js + Complete build with: + - Switch drag wiring + - Start/Stop ramps + 86G trip/reset w/ flag color + - Gate nudge (Knob_65) + - Speed permissive + - Sync-check permissive (±3% V, ±0.15 Hz, <10°) + - Gen kV model + gauge (GenVolts_Rotation: 36°=0, 180°=13, 324°=15) + - Manual voltage (Knob_90); AVR only acts after 52G closes + - Digital readouts: Value_MW / Value_AMPS / Value_MVAR / Value_PowerFactor + - Hz needle + RPM text + - Status/permissive glows + - Syncroscope needle (SyncScope_Rotation) and TWO sync lamps + Lamps brightness ∝ |Vg∠θg − Vb∠θb|; both lamps identical behavior +*/ + +/* ///////////// Section 2 — Debug Harness ///////////// */ +/* ///////////// Section 2.A __setDebugWindow ///////////// */ +let __DEBUG_WIN = null; +function __setDebugWindow(win){ __DEBUG_WIN = win; } + +/* ///////////// Section 2.B logDebug ///////////// */ +function logDebug(message){ + try { + try { console.log(message); } catch(_) {} + const el = document.getElementById('Debug_Log'); + if (el){ + if (typeof el.value === 'string'){ + el.value += (el.value ? "\n" : "") + String(message); + } else { + el.textContent = (el.textContent ? el.textContent + "\n" : "") + String(message); + } + } + if(__DEBUG_WIN && !__DEBUG_WIN.closed){ + __DEBUG_WIN.postMessage(String(message), '*'); + } + } catch(_) {} +} + +/* ///////////// Section 3 — Utilities ///////////// */ +/* ///////////// Section 3.A clamp / lerp / normAngleRad / deg2rad / rad2deg ///////////// */ +function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); } +function lerp(a,b,t){ return a + (b - a) * t; } +function normAngleRad(a){ + // normalize to (-π, +π] + a = ((a + Math.PI) % (2*Math.PI) + 2*Math.PI) % (2*Math.PI) - Math.PI; + return a; +} +function deg2rad(d){ return d * Math.PI / 180; } +function rad2deg(r){ return r * 180 / Math.PI; } + +/* ///////////// Section 4 — Switch Drag/Rotate Wiring (hit zones) ///////////// */ +/* ///////////// Section 4.A Config & globals (switches, sensitivity, knobStates) ///////////// */ +const switches = [ + { parentId:'52G_Switch', knobId:'Knob_52G', upperHitId:'Hit_52G_Upper', lowerHitId:'Hit_52G_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, + { parentId:'41_Switch', knobId:'Knob_41', upperHitId:'Hit_41_Upper', lowerHitId:'Hit_41_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, + { parentId:'86G_Switch', knobId:'Knob_86G', upperHitId:'Hit_86G_Upper', lowerHitId:'Hit_86G_Lower', type:'latching', minAngle:-45, maxAngle:0 }, + { parentId:'AVR_Switch', knobId:'Knob_AVR', upperHitId:'Hit_AVR_Upper', lowerHitId:'Hit_AVR_Lower', type:'latching', minAngle:-45, maxAngle:45 }, + { parentId:'Sync_Switch', knobId:'Knob_Sync', upperHitId:'Hit_Sync_Upper', lowerHitId:'Hit_Sync_Lower', type:'latching', minAngle:-45, maxAngle:45 }, + { parentId:'Master_Switch',knobId:'Knob_Master',upperHitId:'Hit_Master_Upper',lowerHitId:'Hit_Master_Lower',type:'momentary', minAngle:-45, maxAngle:45 }, + { parentId:'65_Switch', knobId:'Knob_65', upperHitId:'Hit_65_Upper', lowerHitId:'Hit_65_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, + { parentId:'90_Switch', knobId:'Knob_90', upperHitId:'Hit_90_Upper', lowerHitId:'Hit_90_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, +]; +const sensitivity = 0.5; // deg per px +const knobStates = {}; + +/* ///////////// Section 4.B Per-switch wiring & event handlers (setAngle/onMove/onUp) ///////////// */ +switches.forEach(cfg => { + const svg = document.getElementById(cfg.parentId); + const knob = document.getElementById(cfg.knobId); + const hitU = document.getElementById(cfg.upperHitId); + const hitL = document.getElementById(cfg.lowerHitId); + if (!svg || !knob || !hitU || !hitL) return; + + const bb = svg.getBBox(); + const cx = bb.x + bb.width/2; + const cy = bb.y + bb.height/2; + + knobStates[cfg.knobId] = { isDragging:false, startX:0, currentAngle:0, centerX:cx, centerY:cy, minAngle:cfg.minAngle, maxAngle:cfg.maxAngle, type:cfg.type }; + + function setAngle(knobId, ang){ + knobStates[knobId].currentAngle = ang; + knob.setAttribute('transform', `rotate(${ang} ${cx} ${cy})`); + } + + hitU.addEventListener('mousedown', (e)=>{ + e.preventDefault(); + knobStates[cfg.knobId].isDragging = true; + knobStates[cfg.knobId].startX = e.clientX; + document.addEventListener('mousemove', onMoveU); + document.addEventListener('mouseup', onUp); + }); + function onMoveU(e){ + if(!knobStates[cfg.knobId].isDragging) return; + const dx = e.clientX - knobStates[cfg.knobId].startX; + const ang = clamp(dx * sensitivity, cfg.minAngle, cfg.maxAngle); + setAngle(cfg.knobId, ang); + } + + hitL.addEventListener('mousedown', (e)=>{ + e.preventDefault(); + knobStates[cfg.knobId].isDragging = true; + knobStates[cfg.knobId].startX = e.clientX; + document.addEventListener('mousemove', onMoveL); + document.addEventListener('mouseup', onUp); + }); + function onMoveL(e){ + if(!knobStates[cfg.knobId].isDragging) return; + const dx = e.clientX - knobStates[cfg.knobId].startX; + const ang = clamp(-dx * sensitivity, cfg.minAngle, cfg.maxAngle); + setAngle(cfg.knobId, ang); + } + + function onUp(){ + if(!knobStates[cfg.knobId].isDragging) return; + knobStates[cfg.knobId].isDragging = false; + + // Improved: allow special return angle if defined + if (cfg.type === 'momentary') { + const returnAngle = (knobStates[cfg.knobId].momentaryReturnAngle != null) + ? knobStates[cfg.knobId].momentaryReturnAngle + : 0; + setAngle(cfg.knobId, returnAngle); + } + + document.removeEventListener('mousemove', onMoveU); + document.removeEventListener('mousemove', onMoveL); + document.removeEventListener('mouseup', onUp); +} + +}); + +/* ///////////// Section 4.C angleOf helper ///////////// */ +function angleOf(id){ + try{ + if (knobStates[id] && typeof knobStates[id].currentAngle === 'number') return knobStates[id].currentAngle; + }catch(_){} + const el = document.getElementById(id); + if(!el) return 0; + const t = el.getAttribute('transform') || ''; + const m = t.match(/rotate\(([-\d.]+)/); + return m ? parseFloat(m[1]) : 0; +} + +/* ///////////// Section 5 — Simulator Core (IIFE) ///////////// */ +(function(){ + /* ///////////// Section 5.A Rated constants (RATED) ///////////// */ + const RATED = { + KV_LL: 13.8, // kV line-line nominal + MVA: 25, // MVA rating + MW: 23.5, // continuous MW + MVAR_LAG_MAX: 15.5, // +Q + MVAR_LEAD_MAX: 19.4, // -Q + AMPS: 1046.9 // at 13.8kV + }; + + /* ///////////// Section 5.B State object (state) + exposure ///////////// */ +const state = { + Master_Started:false, + AVR_On:false, + Sync_On:false, + + // NEW: Latches TRUE when 52G closes; does NOT auto-unlatch on open + GeneratorOnline:false, + + '41_Brk_Var':false, // field breaker + '52G_Brk_Var':false, // gen breaker (FALSE=open, TRUE=closed) + '86G_Trip_Var':false, + + Gate_Pos_Var:0, // % + Gen_Freq_Var:0, // Hz + Gen_RPM_Var:0, // calc + + Speed_Perm_Var:false, + SyncCheck_Perm_Var:false, + + // Bus references + Bus_Freq_Hz:60, + Bus_Voltage_kV:13.8, + + // Voltage model + Gen_kV_Var:0, // actual terminal kV “inside the 52G” + Gen_kV_SP:13.5, // operator/AVR setpoint (kV) + + // Power model + MW:0, + MVAR:0, + AMPS:0, + PF:0 +}; +try{ window.SimState = state; }catch(_){} + + + /* ///////////// Section 5.C Gate setpoint variable (Gate_Setpoint) ///////////// */ + let Gate_Setpoint = 0; + + /* ///////////// Section 5.D Master start/stop ramps (gateRamp/stopRamp) ///////////// */ + const gateRamp = { active:false, from:0, to:19.83, dur:3000, t0:0 }; + const stopRamp = { active:false, from:0, to:0, dur:2000, t0:0 }; // ramp-to-zero on STOP + + /* ///////////// Section 5.E Voltage slew params (KV_* constants) ///////////// */ +const KV_SLEW_MANUAL = 2; // kV/s tracking rate to SP (manual) +const KV_SLEW_AUTO = 1.2; // kV/s when AVR is acting +const KV_MIN = 0.0, KV_MAX = 16.0; + +/* Frequency tuning (single-mode) */ +const FREQ_PER_GATE = 3; // Hz per % gate when rising +const FREQ_DECEL_HZ_S = 3; // fixed fall rate (Hz/s) when raw < current + +// AVR line-drop compensation (disabled if 0) +const AVR_LDC_PU = 0.00; + +// Manual (AVR OFF) droop: pu kV sag per 1.0 pu stator current +const MANUAL_DROOP_PU = 0.12; // try 0.08–0.15 +const MANUAL_Q_GAIN = 0.0; // extra sag for lagging vars (0..1) + +// Power mapping: physical gate needed for ~0 MW when paralleled +const NO_LOAD_GATE_PCT = 20; // set near your sync gate (e.g., 18–20) +const REV_PWR_LIMIT_MW = -5; // cap reverse power (negative) + +// Manual-close PF/MW targets (used when AVR is OFF, at 52G close) +const CLOSE_REV_PWR_TARGET_MW = -0.01; +const CLOSE_PF_TARGET = -0.95; + +// Small manual kV bias at close (optional) +const MANUAL_CLOSE_V_BIAS_KV = 12.5; + +// Reactive gain shaping (keeps vars small near zero load) +const Q_GAIN_MIN = 2.0; // MVAR per kV near zero-load +const Q_GAIN_MAX = 30.0; // MVAR per kV at high load +const Q_GAIN_SHAPE_N = 1.0; // 1=linear + +// ---------- Loss Of Excitation (41 OPEN while 52G CLOSED) tunables ---------- +const LOE_REV_PWR_MW = -0.4; // small reverse MW on field loss +const LOE_Q_IMPORT_MVAR = 15.0; // inductive vars imported on field loss (+ = lagging) +const LOE_SETTLE_MS = 250; // time constant to settle MW/Q to targets + + + + + + + + + /* ///////////// Section 5.F Angle watch thresholds & maps (THRESH/WATCH/prevAngles) ///////////// */ +const THRESH = { up:20, down:-20 }; +const WATCH = [ + { knobIds:['Knob_Master'], upper:'MASTER_START', lower:'MASTER_STOP' }, + { knobIds:['Knob_AVR'], upper:'AVR_ON', lower:'AVR_OFF' }, + { knobIds:['Knob_Sync'], upper:'SYNC_ON', lower:'SYNC_OFF' }, + { knobIds:['Knob_41'], upper:'41_CLOSE', lower:'41_OPEN' }, + { knobIds:['Knob_52G'], upper:'52G_CLOSE', lower:'52G_OPEN' }, + { knobIds:['Knob_86G','Knob_86'], special:'86G' }, +]; +const prevAngles = Object.create(null); + +/* ///////////// Section 5.G setFlag86 (REPLACE this section) ///////////// */ +function setFlag86(){ + try{ + const el = document.getElementById('Flag_86G'); + if (!el) return; + + const k = (typeof knobStates !== 'undefined') ? knobStates['Knob_86G'] : null; + const angle = (k && typeof k.currentAngle === 'number') ? k.currentAngle : 0; + + // < -44° = dark orange; > -1° = default color + el.style.fill = (angle < -44) ? '#CD5A00' : ''; + }catch(_){} +} + + /* ///////////// Section 5.H handleAction (all operator actions) ///////////// */ +function handleAction(tag){ + switch(tag){ + /* ---- Syncroscope switch ---- */ + case 'SYNC_ON': + if (!state.Sync_On){ state.Sync_On = true; try { logDebug('SYNCROSCOPE: ON'); } catch(_){} } + break; + case 'SYNC_OFF': + if (state.Sync_On){ state.Sync_On = false; try { logDebug('SYNCROSCOPE: OFF'); } catch(_){} } + break; + + /* ---- AVR switch ---- */ + case 'AVR_ON': + if (!state.AVR_On){ state.AVR_On = true; try { logDebug('AVR: AUTO'); } catch(_){} } + break; + case 'AVR_OFF': + if (state.AVR_On){ state.AVR_On = false; try { logDebug('AVR: MANUAL'); } catch(_){} } + break; + + /* ---- Master ---- */ + case 'MASTER_START': { + // 86G knob permissive + not tripped + const k86 = (typeof knobStates !== 'undefined') ? knobStates['Knob_86G'] : null; + const ang86 = (k86 && typeof k86.currentAngle === 'number') ? k86.currentAngle : 0; + if (ang86 <= -1){ try{ logDebug('Master: BLOCKED (86G Permissive)'); }catch(_){ } break; } + if (state['86G_Trip_Var']){ try{ logDebug('Master: BLOCKED (86G Tripped)'); }catch(_){ } break; } + + // prevent double staging + if (state.__prestartBusy) break; + state.__prestartBusy = true; + if (!Array.isArray(state.__prestartTimers)) state.__prestartTimers = []; + const T = state.__prestartTimers; + const at = (ms, fn) => T.push(setTimeout(fn, ms)); + + try{ logDebug('Master: START'); }catch(_){} + + // +1.0s — permissives + at(1000, () => { + const perm52 = !state['52G_Brk_Var']; + if (perm52) try{ logDebug('52G Permissive OK'); }catch(_){} + const k86s = knobStates?.['Knob_86G']; + const ang86s = (k86s && typeof k86s.currentAngle === 'number') ? k86s.currentAngle : 0; + if (ang86s > -1) try{ logDebug('86G Permissive OK'); }catch(_){} + }); + + // +2.0s — lift pump + at(2000, () => { try{ logDebug('Lift Pump On'); }catch(_){}; }); + + // +4.0s — pressure ok + at(4000, () => { try{ logDebug('Lift Pump Pressure OK'); }catch(_){}; }); + + // +5.0s — brakes release + at(5000, () => { try{ logDebug('Brakes Released'); }catch(_){}; }); + + // +5.1s — handoff to normal start + at(5100, () => { + const k86f = knobStates?.['Knob_86G']; + const ang86f = (k86f && typeof k86f.currentAngle === 'number') ? k86f.currentAngle : 0; + const ok86 = (ang86f > -1) && !state['86G_Trip_Var']; + if (!ok86){ state.__prestartBusy = false; return; } + + if (!state.Master_Started){ + state.Master_Started = true; + stopRamp.active = false; + gateRamp.active = true; + gateRamp.from = (typeof Gate_Setpoint === 'number') ? Gate_Setpoint : 0; + gateRamp.to = 19.83; + gateRamp.dur = 3000; + gateRamp.t0 = performance.now(); + } + + state.__prestartBusy = false; + while (T.length) clearTimeout(T.pop()); + }); + + break; + } + + case 'MASTER_STOP': { + try{ logDebug('Master: STOP'); }catch(_){} + // cancel any staged start sequence + state.__prestartBusy = false; + if (Array.isArray(state.__prestartTimers)) { + while (state.__prestartTimers.length) clearTimeout(state.__prestartTimers.pop()); + } + // start STOP ramp + gateRamp.active = false; + stopRamp.active = true; + stopRamp.from = (typeof Gate_Setpoint === 'number') ? Gate_Setpoint : 0; + stopRamp.to = 0; + stopRamp.dur = 2000; + stopRamp.t0 = performance.now(); + break; + } + + /* ---- 41 Field breaker ---- */ + case '41_CLOSE': + if(!state['41_Brk_Var']){ + if (state.Speed_Perm_Var){ + state['41_Brk_Var'] = true; + try{ logDebug('Field Breaker: CLOSED'); }catch(_){} + if (state['52G_Brk_Var'] && state.AVR_On){ + state.Gen_kV_SP = 13.8; + state.Gen_kV_Var = state.Gen_kV_SP; + } else { + state.Gen_kV_SP = 13.65; + state.Gen_kV_Var = state.Gen_kV_SP; + } + } else { + try{ logDebug('41: BLOCKED'); }catch(_){} + } + } + break; + + case '41_OPEN': + if(state['41_Brk_Var']){ + state['41_Brk_Var'] = false; + try{ logDebug('Field Breaker: OPEN'); }catch(_){} + } + break; + + /* ---- 52G Generator breaker ---- */ + case '52G_CLOSE': + if(!state['52G_Brk_Var']){ + if (state.SyncCheck_Perm_Var){ + state['52G_Brk_Var'] = true; + try{ logDebug('52G: CLOSED'); }catch(_){} + + // Latch "online" (no auto-unlatch) + if (!state.GeneratorOnline){ + state.GeneratorOnline = true; + try { logDebug('Unit Online'); } catch(_){} + } + + // No-load calibration at close + (function(){ + const slope = 100 / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)); + const MWpu = (CLOSE_REV_PWR_TARGET_MW) / (RATED.MW || 1); + const effNeededPct = (MWpu * 100) / slope; // negative for reverse + state.NoLoadGateCal = clamp(state.Gate_Pos_Var - effNeededPct, 0, 100); + })(); + + if (state.AVR_On){ + state.Gen_kV_SP = 13.8; + state.Gen_kV_Var = state.Gen_kV_SP; + } else { + // manual bias at close + const S = (RATED.MVA || 1); + const pAbs = Math.abs(CLOSE_REV_PWR_TARGET_MW); + const qAbs = Math.sqrt(Math.max(0, S*S - pAbs*pAbs)); + const qTarget = (CLOSE_PF_TARGET < 0 ? -qAbs : qAbs); + const qGainClose = Q_GAIN_MIN; + let dvBias = (qGainClose > 1e-6) ? (qTarget / qGainClose) : 0; + dvBias = clamp(dvBias + MANUAL_CLOSE_V_BIAS_KV, -0.5, 0.5); + state.Gen_kV_SP = Math.max(0, (+state.Gen_kV_SP || 0) + dvBias); + state.Gen_kV_Var = state.Gen_kV_SP; + } + } + } + break; + + case '52G_OPEN': + if(state['52G_Brk_Var']){ + state['52G_Brk_Var'] = false; + delete state.NoLoadGateCal; + try{ logDebug('52G: OPEN'); }catch(_){} + } + break; + + /* ---- 86G Lockout ---- */ + case '86G_TRIP': + if(!state['86G_Trip_Var']){ + state['86G_Trip_Var'] = true; + setFlag86(true); + if(state['41_Brk_Var']){ state['41_Brk_Var'] = false; try{ logDebug('41: TRIPPED'); }catch(_){} } + if(state['52G_Brk_Var']){ state['52G_Brk_Var'] = false; try{ logDebug('52G: TRIPPED'); }catch(_){} } + gateRamp.active = false; + stopRamp.active = true; + stopRamp.from = (typeof Gate_Setpoint === 'number') ? Gate_Setpoint : 0; + stopRamp.to = 0; + stopRamp.dur = 5000; + stopRamp.t0 = performance.now(); + try{ logDebug('86G: TRIP'); }catch(_){} + } + break; + + case '86G_RESET': + if(state['86G_Trip_Var']){ + state['86G_Trip_Var'] = false; + setFlag86(false); + try{ logDebug('86G: RESET'); }catch(_){} + } + break; + } +} + + + +/* ///////////// Section 5.H.1 Enforce 86G Permissive on MASTER_START (add-on) ///////////// */ +/* Place this AFTER Section 5.H (so handleAction exists). */ +(function Enforce86GPermOnStart(){ + const S = window.SimState || window.state || (window.state = {}); + const _old = window.handleAction; + + function readKnob86Angle(){ + // preferred: knobStates cache + const ks = (window.knobStates && window.knobStates['Knob_86G']) || null; + if (ks && typeof ks.currentAngle === 'number') return ks.currentAngle; + + // fallback: parse DOM transform + const el = document.getElementById('Knob_86G'); + if (el){ + const tr = el.getAttribute('transform') || ''; + const m = tr.match(/rotate\((-?\d+(?:\.\d+)?)/i); + if (m) return parseFloat(m[1]); + } + return 0; // default safe + } + + function get86GPermissive(){ + const ang = readKnob86Angle(); + const perm = (ang > -1); // same threshold used for Glow_Perm_86G + S['86G_Perm_Var'] = perm; // keep a state bit for reuse + return perm; + } + + window.handleAction = function(tag){ + if (tag === 'MASTER_START'){ + // Block if 86G knob not in NORMAL or lockout is tripped + const perm86 = get86GPermissive(); + if (!perm86){ + try{ logDebug('Master: BLOCKED (86G Permissive)'); }catch(_){} + return; + } + if (S['86G_Trip_Var'] === true){ + try{ logDebug('Master: BLOCKED (86G Tripped)'); }catch(_){} + return; + } + } + return _old ? _old.apply(this, arguments) : undefined; + }; +})(); + + +/* ///////////// Section 5.H.2 AVR takeover harmonizer (non-invasive) ///////////// */ +(function AVRTakeoverHarmonizer(){ + if (typeof window === 'undefined') return; + const origHandle = (typeof window.handleAction === 'function') ? window.handleAction : null; + if (!origHandle) return; + + window.handleAction = function(tag){ + // Pre-transition context + const wasAVR = !!(state && state.AVR_On); + + // On enabling AVR while paralleled, freeze SP to the present terminal kV + if (tag === 'AVR_ON' && !wasAVR && state && state['52G_Brk_Var']) { + const kv = +state.Gen_kV_Var || 0; + if (typeof clamp === 'function') { + state.Gen_kV_SP = clamp(kv, KV_MIN, KV_MAX); + } else { + state.Gen_kV_SP = Math.min(KV_MAX, Math.max(KV_MIN, kv)); + } + + } + + // On disabling AVR while field or tie is live, freeze SP to current kV + if (tag === 'AVR_OFF' && wasAVR && state && (state['41_Brk_Var'] || state['52G_Brk_Var'])) { + const kv = Math.max(0, +state.Gen_kV_Var || 0); + state.Gen_kV_SP = kv; // manual mode allows up to current value (upper bound handled elsewhere) + + } + + // Delegate to original action handler + return origHandle.call(this, tag); + }; +})(); + + + + + /* ///////////// Section 5.I watchAngles ///////////// */ + function watchAngles(){ + for(const w of WATCH){ + let ang = null; + for(const id of w.knobIds){ + const a = angleOf(id); + if(a !== 0 || document.getElementById(id)){ ang = a; break; } + } + if(ang === null) continue; + const key = w.knobIds[0]; + const prev = prevAngles[key] ?? ang; + + if (w.special === '86G'){ + const T = -1; // degrees + const hadPrev = Object.prototype.hasOwnProperty.call(prevAngles, key); + + if (!hadPrev){ + // First run: align state to current knob position + if (ang <= T) { handleAction('86G_TRIP'); } + else { handleAction('86G_RESET'); } + } else { + // Subsequent frames: edge-detect crossings + if (prev > T && ang <= T) handleAction('86G_TRIP'); + if (prev <= T && ang > T) handleAction('86G_RESET'); + } +} else { + if (prev < THRESH.up && ang >= THRESH.up) handleAction(w.upper); + if (prev > THRESH.down && ang <= THRESH.down) handleAction(w.lower); +} + prevAngles[key] = ang; + } + } + + /* ///////////// Section 5.J updateGateSet (Knob_65) ///////////// */ + function updateGateSet(){ + const NUDGE_THRESH = 20; // degrees + const NUDGE_RATE_OPEN = 0.125; // %/s when 52G OPEN + const NUDGE_RATE_CLOSED = 10; // %/s when 52G CLOSED + const NUDGE_RATE = state['52G_Brk_Var'] ? NUDGE_RATE_CLOSED : NUDGE_RATE_OPEN; + + const now = performance.now(); + if (typeof updateGateSet._tPrev !== 'number') updateGateSet._tPrev = now; + const dt = Math.min(150, now - updateGateSet._tPrev) / 1000; + updateGateSet._tPrev = now; + + const a65 = angleOf('Knob_65') || 0; + + if (a65 >= NUDGE_THRESH){ + Gate_Setpoint = Math.min(100, Gate_Setpoint + NUDGE_RATE * dt); + if (updateGateSet._lastLog == null || Math.abs(Gate_Setpoint - updateGateSet._lastLog) >= 0.5){ + updateGateSet._lastLog = Gate_Setpoint; + } +} else if (a65 <= -NUDGE_THRESH){ + Gate_Setpoint = Math.max(0, Gate_Setpoint - NUDGE_RATE * dt); + if (updateGateSet._lastLog == null || Math.abs(Gate_Setpoint - updateGateSet._lastLog) >= 0.5){ + updateGateSet._lastLog = Gate_Setpoint; + } + } + } + + /* ///////////// Section 5.K updateVoltageSet (Knob_90) ///////////// */ +function updateVoltageSet(){ + const TH = 20; // deg threshold + const now = performance.now(); + if (typeof updateVoltageSet._tPrev !== 'number') updateVoltageSet._tPrev = now; + const dt = Math.min(150, now - updateVoltageSet._tPrev) / 1000; + updateVoltageSet._tPrev = now; + + const a90 = angleOf('Knob_90') || 0; + const NU = 0.01; // sensitivity + const RATE = NU * (KV_MAX - KV_MIN); // kV/s equivalent + + if (a90 >= TH){ + state.Gen_kV_SP = state.AVR_On + ? clamp(state.Gen_kV_SP + RATE*dt, KV_MIN, KV_MAX) + : Math.max(0, state.Gen_kV_SP + RATE*dt); // no upper limit when AVR OFF + } else if (a90 <= -TH){ + state.Gen_kV_SP = state.AVR_On + ? clamp(state.Gen_kV_SP - RATE*dt, KV_MIN, KV_MAX) + : Math.max(0, state.Gen_kV_SP - RATE*dt); // allow down to 0 only + } +} + + + /* ///////////// Section 5.L updateSyncCheck (sync permissive) ///////////// */ +function updateSyncCheck(){ + const V_TOL_FRAC = 0.03; // ±3% + const F_TOL_HZ = 0.15; // ±0.15 Hz + const PHASE_DEG = 10.0; // ±10° + + const snap = PhaseTracker.snap; + const vb = snap ? snap.vb : (+state.Bus_Voltage_kV || 13.8); + const vg = snap ? snap.vg : (+state.Gen_kV_Var || 0); + const fb = snap ? snap.fb : (+state.Bus_Freq_Hz || 60); + const fg = snap ? snap.fg : (+state.Gen_Freq_Var || 0); + const ddeg = snap ? snap.dphiDeg : PhaseTracker.deltaDeg(); + + const vOK = Math.abs(vg - vb) <= V_TOL_FRAC * vb; + const fOK = Math.abs(fg - fb) <= F_TOL_HZ; + const pOK = Math.abs(ddeg) <= PHASE_DEG; + + const ok = !!(vOK && fOK && pOK); + if (ok !== state.SyncCheck_Perm_Var){ + state.SyncCheck_Perm_Var = ok; + + } +} + +/* ///////////// Section 5.M updatePhysics (governor/AVR/power) — REPLACE ENTIRE SECTION ///////////// */ +function updatePhysics(){ + if (!state.Master_Started) { + gateRamp.active = false; + stopRamp.active = false; + + state.Gen_Freq_Var = 0; + state.Gen_RPM_Var = 0; + + const targetKV = state['41_Brk_Var'] ? state.Gen_kV_SP : 0; + slewGenKV(targetKV, KV_SLEW_MANUAL); + + if (state.Speed_Perm_Var !== false) { + state.Speed_Perm_Var = false; + } + + // reset run latch + updatePhysics._wasRunning = false; + + state.MW = 0; state.MVAR = 0; state.AMPS = 0; state.PF = 0; + return; + } + + // Gate setpoint ramps + if (gateRamp.active){ + const p = clamp((performance.now() - gateRamp.t0) / gateRamp.dur, 0, 1); + Gate_Setpoint = gateRamp.from + (gateRamp.to - gateRamp.from) * p; + if (p >= 1){ + gateRamp.active = false; + } + } else if (stopRamp.active){ + const p = clamp((performance.now() - stopRamp.t0) / stopRamp.dur, 0, 1); + Gate_Setpoint = stopRamp.from + (stopRamp.to - stopRamp.from) * p; + if (p >= 1){ + stopRamp.active = false; + Gate_Setpoint = 0; + // do NOT snap gates/freq or clear Master_Started here + } + } + + // Governor: actual gate follows setpoint (separate rates for TRIP vs NORMAL) + const RATE_OPEN_NORMAL = 5 / 1000; // %/ms (≈5 %/s) — used when 52G OPEN + const RATE_CLOSED_NORMAL = 20 / 1000; // %/ms (≈20 %/s) — used when 52G CLOSED + const RATE_OPEN_TRIP = 20 / 1000; // %/ms (≈20 %/s) — TRIP ramp when 52G OPEN + const RATE_CLOSED_TRIP = 80 / 1000; // %/ms (≈80 %/s) — TRIP ramp when 52G CLOSED + + const isTripSlew = !!(stopRamp.active && state['86G_Trip_Var']); + const rate = state['52G_Brk_Var'] + ? (isTripSlew ? RATE_CLOSED_TRIP : RATE_CLOSED_NORMAL) + : (isTripSlew ? RATE_OPEN_TRIP : RATE_OPEN_NORMAL); + + const now = performance.now(); + if (typeof updatePhysics._tPrev !== 'number') updatePhysics._tPrev = now; + const dt = Math.min(100, now - updatePhysics._tPrev); + updatePhysics._tPrev = now; + + const maxStep = rate * dt; + const err = Gate_Setpoint - state.Gate_Pos_Var; + if (Math.abs(err) > maxStep){ + state.Gate_Pos_Var += Math.sign(err) * maxStep; + } else { + state.Gate_Pos_Var = Gate_Setpoint; + } + + // Frequency (single-owner slew): on-grid=60; off-grid rises follow gate; falls decay at fixed rate + { + const onGrid = !!state['52G_Brk_Var']; + const raw = onGrid ? 60 : (FREQ_PER_GATE * state.Gate_Pos_Var); + const curr = +state.Gen_Freq_Var || 0; + const dt_s = Math.max(0, dt) / 1000; + + const next = (raw >= curr) ? raw : Math.max(raw, curr - FREQ_DECEL_HZ_S * dt_s); + + state.Gen_Freq_Var = clamp(next, 0, 94); + state.Gen_RPM_Var = state.Gen_Freq_Var * 1.667; + } + + // Mark as having run (prevents immediate "Unit Stopped" right after Master Start) + { + if (state.Master_Started && (state.Gate_Pos_Var > 0.5 || state.Gen_Freq_Var > 0.2)) { + updatePhysics._wasRunning = true; + } + } + + // Latch "Unit Stopped" only when we are actually stopping AND gates ~0 AND frequency ~0 (off-grid) + { + const CLOSE_LATCH_EPS = 0.5; // % + const FREQ_LATCH_EPS = 0.2; // Hz + const stoppingIntent = stopRamp.active || state['86G_Trip_Var'] || !!updatePhysics._wasRunning; + + if (!state['52G_Brk_Var'] && + state.Master_Started && + stoppingIntent && + state.Gate_Pos_Var <= CLOSE_LATCH_EPS && + state.Gen_Freq_Var <= FREQ_LATCH_EPS) { + state.Gate_Pos_Var = 0; + state.Master_Started = false; + stopRamp.active = false; + updatePhysics._wasRunning = false; + logDebug('Unit Stopped'); + } + } + + // Speed Permissive toggle + const spNext = !!(state.Master_Started && (state.Gate_Pos_Var > 0) && (state.Gen_RPM_Var > 50)); + if (state.Speed_Perm_Var !== spNext) { + state.Speed_Perm_Var = spNext; + } + + // AVR & kV tracking + const Vbus = state.Bus_Voltage_kV || 13.8; + if (state['52G_Brk_Var']) { + if (state.AVR_On){ + let kvTargetSP = state.Gen_kV_SP; + slewGenKV(kvTargetSP, KV_SLEW_AUTO); + } else { + // MANUAL: droop with load + const Ipu = clamp((state.AMPS || 0) / (RATED.AMPS || 1), 0, 2); + const Qpu = clamp((state.MVAR || 0) / (RATED.MVAR_LAG_MAX || 1), -1, 1); + const extra = 1 + MANUAL_Q_GAIN * Math.max(0, Qpu); + const V_droop = Vbus * MANUAL_DROOP_PU * Ipu * extra; + const target = state.Gen_kV_SP - V_droop; + slewGenKV(target, KV_SLEW_MANUAL); + } + } else { + // Not paralleled: track SP if field on, else decay to 0 + const tgt = state['41_Brk_Var'] ? state.Gen_kV_SP : 0; + slewGenKV(tgt, KV_SLEW_MANUAL); + } + + // Power model (with no-load gate offset; does NOT move the gates) + let MW = 0; + if (state['52G_Brk_Var']){ + const noLoad = (typeof state.NoLoadGateCal === 'number') ? state.NoLoadGateCal : NO_LOAD_GATE_PCT; + const effGate = state.Gate_Pos_Var - noLoad; // can be negative + const slope = 100 / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)); // keep 100% gate => rated MW + let MW_pu = (effGate * slope) / 100; // per-unit MW + const min_pu = (REV_PWR_LIMIT_MW) / (RATED.MW || 1); + MW_pu = clamp(MW_pu, min_pu, 1); + MW = MW_pu * RATED.MW; + } + + // Reactive power: scale gain with effective gate so MVAR is small at close + let Q = 0; + if (state['52G_Brk_Var']){ + const dv = (state.Gen_kV_Var - Vbus); // kV + const noLoad = (typeof state.NoLoadGateCal === 'number') ? state.NoLoadGateCal : NO_LOAD_GATE_PCT; + const effGatePU = clamp( + (state.Gate_Pos_Var - noLoad) / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)), + 0, 1 + ); + const qGain = Q_GAIN_MIN + (Q_GAIN_MAX - Q_GAIN_MIN) * Math.pow(effGatePU, Q_GAIN_SHAPE_N); + Q = clamp(dv * qGain, -RATED.MVAR_LEAD_MAX, RATED.MVAR_LAG_MAX); + } + + // -------- Loss Of Excitation override (52G CLOSED & 41 OPEN) ---------- + if (state['52G_Brk_Var'] && !state['41_Brk_Var']){ + const t = performance.now(); + if (typeof updatePhysics._loeT0 !== 'number') updatePhysics._loeT0 = t; + const α = Math.min(1, (t - updatePhysics._loeT0) / LOE_SETTLE_MS); + + // Blend current values toward LOE targets + MW = MW + (LOE_REV_PWR_MW - MW) * α; // small reverse + Q = Q + (LOE_Q_IMPORT_MVAR - Q) * α; // inductive import (positive) + } else { + delete updatePhysics._loeT0; + } + // ---------------------------------------------------------------------- + + // Apparent + amps + PF + const S = Math.sqrt(MW*MW + Q*Q); + const I_kA = (S) / (Math.sqrt(3) * (Vbus)); + const I_A = I_kA * 1000; + const PFmag = S > 1e-6 ? (Math.abs(MW) / S) : 0; + const PFsigned = (Q < 0 ? -PFmag : PFmag); + + state.MW = MW; + state.MVAR = Q; + state.AMPS = I_A; + state.PF = PFsigned; +} + + + + /* ///////////// Section 5.N slewGenKV (kV tracker) ///////////// */ + function slewGenKV(target, rate_kV_per_s){ + const now = performance.now(); + if (typeof slewGenKV._tPrev !== 'number') slewGenKV._tPrev = now; + const dt = Math.min(100, now - slewGenKV._tPrev)/1000; + slewGenKV._tPrev = now; + const step = rate_kV_per_s * dt; + const err = target - state.Gen_kV_Var; + if (Math.abs(err) <= step){ + state.Gen_kV_Var = target; + } else { + state.Gen_kV_Var += Math.sign(err) * step; + } + state.Gen_kV_Var = clamp(state.Gen_kV_Var, KV_MIN, KV_MAX); + } + + /* ///////////// Section 5.O Syncroscope & Lamps ///////////// */ +/* ///////////// Section 5.O.0 PhaseTracker (shared Δθ + snapshot) ///////////// */ +const PhaseTracker = { + busPhase: 0, + genPhase: 0, + tPrev: null, + snap: null, // { vb, vg, fb, fg, dphiDeg, ts } + + update(fb, fg) { + const t = performance.now() * 0.001; + if (this.tPrev == null) this.tPrev = t; + let dt = t - this.tPrev; + if (dt > 0.2) dt = 0.2; + if (dt < 0) dt = 0; + this.tPrev = t; + + this.busPhase = (this.busPhase + 2 * Math.PI * (fb || 0) * dt) % (2 * Math.PI); + this.genPhase = (this.genPhase + 2 * Math.PI * (fg || 0) * dt) % (2 * Math.PI); + return this.deltaDeg(); + }, + + deltaDeg() { + const dphi = normAngleRad(this.genPhase - this.busPhase); // (-π,π] + return rad2deg(dphi); // (-180,180] + }, + + snapshot(vb, vg, fb, fg) { + this.snap = { + vb: (vb != null ? vb : 13.8), + vg: Math.max(0, vg || 0), + fb: (fb != null ? fb : 60), + fg: (fg || 0), + dphiDeg: this.deltaDeg(), + ts: performance.now() + }; + return this.snap; + } +}; + + /* ///////////// Section 5.O.1 SYNC struct ///////////// */ + const SYNC = { + needleEl: null, center: null, + lampL: null, lampR: null, + busPhase: 0, genPhase: 0, tPrev: null + }; + + /* ///////////// Section 5.O.2 parseRotateCenter ///////////// */ + function parseRotateCenter(el) { + const t = (el.getAttribute("transform") || "").trim(); + const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); + if (m) return { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; + try { const bb = el.getBBox(); return { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } + catch { return { cx: 0, cy: 0 }; } + } + + /* ///////////// Section 5.O.3 initSyncUI ///////////// */ + function initSyncUI(){ + if (!SYNC.needleEl){ + SYNC.needleEl = document.getElementById("SyncScope_Rotation"); + if (SYNC.needleEl) SYNC.center = parseRotateCenter(SYNC.needleEl); + } + if (!SYNC.lampL) SYNC.lampL = document.getElementById('Glow_SyncLeft'); + if (!SYNC.lampR) SYNC.lampR = document.getElementById('Glow_SyncRight'); + } + +/* ///////////// Section 5.O.4 updateSyncScopeAndLamps ///////////// */ +function updateSyncScopeAndLamps(){ + initSyncUI(); + const snap = PhaseTracker.snap; + if (!snap) return; + + const DEFAULT_SYNC_ANGLE = 0; // needle's default clamped angle + const fg = +state.Gen_Freq_Var || 0; + const syncSwitchOff = !state.Sync_On; + const breaker52Closed = !!state['52G_Brk_Var']; // TRUE = closed + const clampNeedle = (Math.abs(fg) < 0.01) || syncSwitchOff || breaker52Closed; + + // Needle angle (clamped when Hz=0 OR Sync switch OFF OR 52G closed) + if (SYNC.needleEl && SYNC.center){ + const angle = clampNeedle + ? DEFAULT_SYNC_ANGLE + : ((snap.dphiDeg % 360) + 360) % 360; // 0..360 from shared Δθ + SYNC.needleEl.setAttribute('transform', `rotate(${angle},${SYNC.center.cx},${SYNC.center.cy})`); + } + + // Lamps brightness from true differential voltage magnitude + (function(){ + const clamp01 = x => x < 0 ? 0 : (x > 1 ? 1 : x); + + const Vb = snap.vb; + const Vg = snap.vg; + const dphiRad = deg2rad(snap.dphiDeg); + + // |Vg∠θg − Vb∠θb| + const Vr_kV = Math.sqrt(Vg*Vg + Vb*Vb - 2*Vg*Vb*Math.cos(dphiRad)); + + const Vnom = (state.Bus_Voltage_kV_nom != null ? state.Bus_Voltage_kV_nom : 13.8); + const Vr_pu = Vr_kV / (Vnom || 1); + + const LAMP_GAIN = 2; + const GAMMA = 1.1; + const SYNC_FUDGE_MIN_KV = 13.2; + const SYNC_FUDGE_MAX_KV = 14.5; + + let bright = clamp01(Math.pow(clamp01(Vr_pu * LAMP_GAIN), GAMMA)); + + // Fudge band: outside band = full ON + const inBand = (Vg > SYNC_FUDGE_MIN_KV) && (Vg < SYNC_FUDGE_MAX_KV); + if (!inBand) bright = 1.0; + + // Only show while syncing (Sync switch ON, 52G OPEN) + const syncing = !!state.Sync_On && !state['52G_Brk_Var']; + if (!syncing) bright = 0; + + if (SYNC.lampL){ SYNC.lampL.style.opacity = String(bright); SYNC.lampL.setAttribute('fill', '#FFFFFF'); } + if (SYNC.lampR){ SYNC.lampR.style.opacity = String(bright); SYNC.lampR.setAttribute('fill', '#FFFFFF'); } + })(); +} + + /* ///////////// Section 5.P Glow helpers (setGlow / setGlowWhite) ///////////// */ + function setGlow(id, on){ + const el = document.getElementById(id); + if(!el) return; + el.style.opacity = on ? '1' : '0'; + } + function setGlowWhite(id, on){ + const el = document.getElementById(id); + if(!el) return; + el.style.fill = on ? '#FFFFFF' : ''; + el.style.opacity = on ? '1' : '0'; + } + + /* ///////////// Section 5.Q updateGlows (REPLACE this section) ///////////// */ +function updateGlows(){ + const is52Open = !state['52G_Brk_Var']; + const is41Closed = !!state['41_Brk_Var']; + + // 86G permissive & flag based ONLY on knob position + const k86 = (typeof knobStates !== 'undefined') ? knobStates['Knob_86G'] : null; + const ang = (k86 && typeof k86.currentAngle === 'number') ? k86.currentAngle : 0; + const is86NormalByKnob = (ang > -1); + + // Permissives + setGlow('Glow_Perm_52G', is52Open); + setGlow('Glow_Perm_86G', is86NormalByKnob); + setGlow('Glow_Perm_Speed', !!state.Speed_Perm_Var); + setGlow('Glow_Perm_Excitation', is41Closed); + setGlowWhite('Glow_Perm_SyncCheck', !!state.SyncCheck_Perm_Var); // white + + // 52G status + setGlow('Glow_Green_52G', is52Open); + setGlow('Glow_Red_52G', !is52Open); + + // 41 status + setGlow('Glow_Green_41', !is41Closed); + setGlow('Glow_Red_41', is41Closed); + + // Ensure 86G flag tracks knob position continuously + try { setFlag86(); } catch(_){} +} + + + /* ///////////// Section 5.R updateGateGauge ///////////// */ + // Gate position (GatePos_Rotation, 0%->36°, 100%->324°) + function updateGateGauge(){ + const el = document.getElementById("GatePos_Rotation"); + if (!el) return; + const pct = clamp(state.Gate_Pos_Var, 0, 100); + const minAng = 36, maxAng = 324; + const ang = minAng + (pct / 100) * (maxAng - minAng); + const center = (function(){ + const t = (el.getAttribute("transform") || "").trim(); + const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); + if (m) return { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; + try { const bb = el.getBBox(); return { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } + catch { return { cx: 0, cy: 0 }; } + })(); + el.setAttribute("transform", `rotate(${ang},${center.cx},${center.cy})`); + } + + /* ///////////// Section 5.S Hz needle binding (IIFE) ///////////// */ + (function(){ + const PTS = [ + {hz: 0, ang: 36}, + {hz: 59, ang: 108}, + {hz: 60, ang: 180}, + {hz: 61, ang: 252}, + {hz: 62, ang: 324}, + ]; + function mapHzToAngle(hz) { + if (!isFinite(hz)) return 36; + hz = clamp(hz, 0, 62); + for (let i = 0; i < PTS.length - 1; i++) { + const a = PTS[i], b = PTS[i+1]; + if (hz >= a.hz && hz <= b.hz) { + const t = (hz - a.hz) / (b.hz - a.hz || 1); + return a.ang + t * (b.ang - a.ang); + } + } + return 324; + } + let hzEl = null, center = null; + function initHz() { + hzEl = document.getElementById("Hz_Rotation"); + if (!hzEl) return false; + const t = (hzEl.getAttribute("transform") || "").trim(); + const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); + if (m) center = { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; + else { + try { const bb = hzEl.getBBox(); center = { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } + catch { center = { cx: 0, cy: 0 }; } + } + return true; + } + function updateRPMText() { + const el = document.getElementById("Value_RPM"); + if (!el) return; + const v = Math.round(state.Master_Started ? (state.Gen_RPM_Var || 0) : 0); + if (el.textContent !== String(v)) el.textContent = String(v); + } + function updateHz() { + if (!hzEl && !initHz()) return; + const angle = mapHzToAngle(state.Master_Started ? (state.Gen_Freq_Var || 0) : 0); + hzEl.setAttribute("transform", `rotate(${angle},${center.cx},${center.cy})`); + } + function tick(){ updateRPMText(); updateHz(); } + document.addEventListener("DOMContentLoaded", () => { initHz(); tick(); }); + clearInterval(window.__v6k_iv); + window.__v6k_iv = setInterval(tick, 100); + })(); + + /* ///////////// Section 5.T updateKVgauge ///////////// */ + // Gen Volts gauge (GenVolts_Rotation: 36°=0 kV, 180°=13 kV, 324°=15 kV) + function updateKVgauge(){ + const el = document.getElementById("GenVolts_Rotation"); + if (!el) return; + const v = clamp(state.Gen_kV_Var, 0, 15); // gauge tops at 15 + let ang; + if (v <= 13){ + // map 0..13 kV to 36..180° + const t = v / 13; + ang = 36 + t * (180 - 36); + } else { + // map 13..15 to 180..324° + const t = (v - 13) / 2; + ang = 180 + t * (324 - 180); + } + const center = (function(){ + const t = (el.getAttribute("transform") || "").trim(); + const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); + if (m) return { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; + try { const bb = el.getBBox(); return { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } + catch { return { cx: 0, cy: 0 }; } + })(); + el.setAttribute("transform", `rotate(${ang},${center.cx},${center.cy})`); + } + + /* ///////////// Section 5.U fmtNoLeadingZeros ///////////// */ + function fmtNoLeadingZeros(n, decimals){ + if (!isFinite(n)) return '0'; + const s = (decimals != null) ? n.toFixed(decimals) : String(Math.round(n)); + return s; + } + + /* ///////////// Section 5.V updateDigitals ///////////// */ +function updateDigitals(){ + const elMW = document.getElementById("Value_MW"); + const elA = document.getElementById("Value_AMPS"); + const elMVAR = document.getElementById("Value_MVAR"); + const elPF = document.getElementById("Value_PowerFactor"); + + const MW = +state.MW || 0; + const MVAR = +state.MVAR || 0; + const AMPS = +state.AMPS || 0; + + // PF display — show "unity at tiny load" only when AVR is ON + const S_MVA = Math.sqrt(MW*MW + MVAR*MVAR); + const PF_SMALL_THRESH_MVA = 0.3; + let pfDisp; + if (state.AVR_On && state['52G_Brk_Var'] && S_MVA < PF_SMALL_THRESH_MVA){ + pfDisp = (MVAR < 0 ? -1 : 1); + } else if (S_MVA < 1e-6){ + pfDisp = 1; + } else { + pfDisp = clamp(+state.PF || 0, -1, 1); + } + + if (elMW){ + const s = fmtNoLeadingZeros(MW, 1); + if (elMW.textContent !== s) elMW.textContent = s; + } + if (elA){ + const s = fmtNoLeadingZeros(AMPS, 0); + if (elA.textContent !== s) elA.textContent = s; + } + if (elMVAR){ + const s = fmtNoLeadingZeros(MVAR, 1); + if (elMVAR.textContent !== s) elMVAR.textContent = s; + } + if (elPF){ + const s = fmtNoLeadingZeros(pfDisp, 2); + if (elPF.textContent !== s) elPF.textContent = s; + } +} + + + +/* ///////////// Section 5.W Main tick loop (requestAnimationFrame) ///////////// */ +function tick(){ + watchAngles(); + updateGateSet(); + updateVoltageSet(); + updatePhysics(); + + // Instant zero kV if 52G CLOSED and 41 OPEN + if (state['52G_Brk_Var'] && !state['41_Brk_Var']) { + state.Gen_kV_Var = 0; + } + + // ---- Shared phase + snapshot (single source of truth) ---- + const fb = +state.Bus_Freq_Hz || 60; + const fg = +state.Gen_Freq_Var || 0; + const vb = +state.Bus_Voltage_kV || 13.8; + const vg = +state.Gen_kV_Var || 0; + PhaseTracker.update(fb, fg); + PhaseTracker.snapshot(vb, vg, fb, fg); + // ---------------------------------------------------------- + + updateSyncCheck(); + updateGlows(); + updateGateGauge(); + updateKVgauge(); + updateSyncScopeAndLamps(); + if (window.Osc && typeof window.Osc.update === 'function') window.Osc.update(); // Oscilloscope + updateDigitals(); + requestAnimationFrame(tick); +} +requestAnimationFrame(tick); + + + +/* ///////////// Section 5.Y — Oscilloscope (Bus & Gen waveforms) ///////////// */ +(function(){ + const Osc = { + inited:false, + els:{ bg:null, bus:null, gen:null }, + rect:null, + busDrawn:false, + + $: id => document.getElementById(id), + + ensureInit(){ + if (this.inited) return true; + this.els.bg = this.$('Osc_Background'); + this.els.bus = this.$('Osc_BusWave'); + this.els.gen = this.$('Osc_GenWave'); + if (!this.els.bg || !this.els.bus || !this.els.gen) return false; + + const x = parseFloat(this.els.bg.getAttribute('x')) || 0; + const y = parseFloat(this.els.bg.getAttribute('y')) || 0; + const w = parseFloat(this.els.bg.getAttribute('width')) || (this.els.bg.getBBox?.().width || 0); + const h = parseFloat(this.els.bg.getAttribute('height')) || (this.els.bg.getBBox?.().height || 0); + this.rect = { x, y, w, h, cx:x+w/2, cy:y+h/2 }; + this.inited = true; + return true; + }, + + sinePath(x, w, cy, cycles, amp, phaseRad, samples){ + const k = 2*Math.PI*cycles; + let d = `M ${x} ${ (cy - amp*Math.sin(phaseRad)).toFixed(1) }`; + for(let i=1;i<=samples;i++){ + const t = i/samples; + const xx = x + t*w; + const yy = cy - amp*Math.sin(k*t + phaseRad); + d += ` L ${xx.toFixed(1)} ${yy.toFixed(1)}`; + } + return d; + }, + + drawBusOnce(){ + if (!this.ensureInit()) return; + if (this.busDrawn) return; + const r = this.rect; + const A = 0.90*(r.h/2); + const cycles = 4; + const d = this.sinePath(r.x, r.w, r.cy, cycles, A, 0, 480); + this.els.bus.setAttribute('d', d); + this.busDrawn = true; + }, + + updateGen(){ + if (!this.ensureInit()) return; + const r = this.rect; + const s = (window.SimState || window.state || {}); + const snap = (typeof PhaseTracker !== 'undefined' && PhaseTracker.snap) ? PhaseTracker.snap : null; + + const fg = +s.Gen_Freq_Var || 0; + const vGen = Math.max(0, +s.Gen_kV_Var || 0); + const vNom = (+s.Bus_Voltage_kV_nom) || (+s.Bus_Voltage_kV) || 13.8; + + const Amax = 0.90*(r.h/2); + const A = Amax * (vGen / (vNom || 13.8)); + + const busHz = (+s.Bus_Freq_Hz) || 60; + const T_win = 4 / busHz; // ~4 bus cycles across width + const cyclesG = fg * T_win; + + // Clamp phase to 0° when 52G is CLOSED (snap in-phase with bus) + const synced = !!s['52G_Brk_Var']; + const phiGen = synced ? 0 : (snap ? deg2rad(snap.dphiDeg) : 0); + + const d = this.sinePath(r.x, r.w, r.cy, cyclesG, A, phiGen, 480); + this.els.gen.setAttribute('d', d); + }, + + update(){ + this.drawBusOnce(); + this.updateGen(); + } + }; + + try { window.Osc = Osc; } catch(_){} +})(); + + +/* ///////////// Section 5.Z Protections (add-on; REPLACE this whole section) ///////////// */ +(function ProtectionsAddon(){ + const S = window.SimState || window.state || (window.state = {}); + Object.assign(S, { + Alarm_32:false, Alarm_55:false, Alarm_27:false, Alarm_59:false, Alarm_81:false, + Trip_32:false, Trip_40:false, Trip_27_59:false, Trip_81:false + }); + + const PROT = (window.__PROT__ = window.__PROT__ || { + tprev: performance.now(), lastNow: 0, dtCache: 0, + prev52: !!S['52G_Brk_Var'], + inhibit32UntilMs: 0, + a32:0, a55:0, a55sev:0, a27:0, a59:0, a81UF:0, a81OF:0, + t32:0, t40A:0, t40B:0, t27:0, t59:0, t27Backup:0, t59Backup:0, t81UF:0, t81OF:0, + }); + + function getDt(){ + const now = performance.now(); + if (now - PROT.lastNow < 5) return PROT.dtCache; + const dt = Math.max(0, (now - PROT.tprev)/1000); + PROT.tprev = now; PROT.lastNow = now; PROT.dtCache = dt; + return dt; + } + + function logFlag(name, on){ + if (S[name] !== on){ + S[name] = on; + + // --- Suppress OLD trip notifications (commented out on purpose) --- + if (name === 'Trip_32' || name === 'Trip_40' || name === 'Trip_27_59' || name === 'Trip_81') { + // old generic trip logs we no longer want: + // try { logDebug(`${name.replaceAll('_',' ').toUpperCase()}: ${on?'TRUE':'FALSE'}`); } catch(_){} + return; // keep logic intact; no debug line + } + // ------------------------------------------------------------------ + + // Alarm messages + if (name.startsWith('Alarm_')){ + const onoff = on ? 'ACTIVE' : 'FALSE'; + const code = name.split('_')[1]; + try { logDebug(`ALARM ${code}: ${onoff}`); } catch(_) {} + return; + } + + // Trip messages (explicit) + let msg = ''; + if (name === 'Trip_32' && on) msg = 'TRIP 32 (Reverse Power) LATCHED'; + if (name === 'Trip_40' && on) msg = '41: TRIPPED'; + if (name === 'Trip_27_59' && on) msg = 'TRIP 27 (Undervoltage) LATCHED; TRIP 59 (Overvoltage) LATCHED'; + if (name === 'Trip_81' && on) msg = 'TRIP 81 (Frequency) LATCHED'; + if (msg){ + try { logDebug(msg); } catch(_) {} + } + } + } + + function setGlow(id, on){ + const el = document.getElementById(id); + if (!el) return; + el.style.opacity = on ? 1 : 0; + try{ el.setAttribute('opacity', on ? '1' : '0'); }catch(_){} + } + + // ---------- Alarms (self-reset) ---------- + window.evaluateAlarms = function(){ + const now = performance.now(); + const dt = getDt(); + + // Master Stop mask: block alarms during normal shutdown + if (S.MasterStopMask === true) { + logFlag('Alarm_32', false); + logFlag('Alarm_55', false); + logFlag('Alarm_27', false); + logFlag('Alarm_59', false); + logFlag('Alarm_81', false); + PROT.a32 = PROT.a55 = PROT.a27 = PROT.a59 = PROT.a81UF = PROT.a81OF = 0; + return; + } + + if (!PROT.prev52 && S['52G_Brk_Var']) PROT.inhibit32UntilMs = now + 15000; + PROT.prev52 = !!S['52G_Brk_Var']; + + if (!S.GeneratorOnline){ + logFlag('Alarm_32', false); + logFlag('Alarm_55', false); + logFlag('Alarm_27', false); + logFlag('Alarm_59', false); + logFlag('Alarm_81', false); + PROT.a32 = PROT.a55 = PROT.a27 = PROT.a59 = PROT.a81UF = PROT.a81OF = 0; + return; + } + + const RatedMW = Math.max(1e-6, +((window.RATED && RATED.MW) || 23.5)); + const MW = +S.MW || 0; + const Q = +S.MVAR || 0; + const Vpu = Math.max(0, (+S.Gen_kV_Var || 0) / ((window.RATED && RATED.KV_LL) || 13.8)); + const Hz = +S.Gen_Freq_Var || 0; + + // 32 — Reverse Power (self-reset alarm) + const ppu = MW / RatedMW; + const a32 = (ppu < -0.01); + PROT.a32 = a32 ? PROT.a32 + dt : 0; + logFlag('Alarm_32', PROT.a32 >= 0.2); + + // 55 — LOE / Field Issues (proxy alarm) + const a55 = (!S['41_Brk_Var'] && S['52G_Brk_Var']); + PROT.a55 = a55 ? PROT.a55 + dt : 0; + logFlag('Alarm_55', PROT.a55 >= 0.2); + + // 27 — Undervoltage (self-reset, immediate) + const a27 = (Vpu < 0.96); + PROT.a27 = a27 ? PROT.a27 + dt : 0; + logFlag('Alarm_27', a27); // use a time threshold here if you want a delay + + // 59 — Overvoltage (self-reset, immediate) + const a59 = (Vpu > 1.04); + PROT.a59 = a59 ? PROT.a59 + dt : 0; + logFlag('Alarm_59', a59); // use a time threshold here if you want a delay + + // 81 — Frequency (5 s either side) + const a81UF = (Hz < 59.8); + const a81OF = (Hz > 60.2); + PROT.a81UF = a81UF ? PROT.a81UF + dt : 0; + PROT.a81OF = a81OF ? PROT.a81OF + dt : 0; + logFlag('Alarm_81', (PROT.a81UF >= 5.0) || (PROT.a81OF >= 5.0)); + }; + + // ---------- Trips (LATCHED) ---------- + window.evaluateTrips = function(){ + const dt = getDt(); + + // Master Stop mask: block trip evaluation during normal shutdown + if (S.MasterStopMask === true) { + return; + } + + if (!S.GeneratorOnline){ + // Latching: do NOT auto-clear trips when offline + return; + } + + const RatedMW = Math.max(1e-6, +((window.RATED && RATED.MW) || 23.5)); + const RatedMVA = Math.max(1e-6, +((window.RATED && RATED.MVA) || RatedMW)); + const MW = +S.MW || 0; + const Q = +S.MVAR || 0; + const Vpu = Math.max(0, (+S.Gen_kV_Var || 0) / ((window.RATED && RATED.KV_LL) || 13.8)); + const Hz = +S.Gen_Freq_Var || 0; + + // 32 — Reverse Power (0.5 s, inhibit for 15s after close) + const now = performance.now(); + const inhibit32 = (now < PROT.inhibit32UntilMs); + PROT.t32 = (MW < -0.02 * RatedMW && !inhibit32) ? PROT.t32 + dt : 0; + if (!S.Trip_32 && PROT.t32 >= 0.5) { + logFlag('Trip_32', true); + } + + // 40 — Field Breaker trip (proxy) + const t40 = (!S['41_Brk_Var'] && S['52G_Brk_Var']); + PROT.t40A = t40 ? PROT.t40A + dt : 0; + PROT.t40B = t40 ? PROT.t40B + dt : 0; + if (!S.Trip_40 && (PROT.t40A >= 0.05 || PROT.t40B >= 0.05)) { + logFlag('Trip_40', true); + } + + // 27/59 — Voltage trips (0.5 s either side) + const v27 = (Vpu < 0.90); + const v59 = (Vpu > 1.10); + PROT.t27 = v27 ? PROT.t27 + dt : 0; + PROT.t59 = v59 ? PROT.t59 + dt : 0; + + const trip27 = (PROT.t27 >= 0.5); + const trip59 = (PROT.t59 >= 0.5); + if ((!S.Trip_27_59) && (trip27 || trip59)) { + logFlag('Trip_27_59', true); + try { + if (trip27) logDebug('TRIP 27 (Undervoltage) LATCHED'); + if (trip59) logDebug('TRIP 59 (Overvoltage) LATCHED'); + } catch(_) {} + } + + // 81 — Frequency + PROT.t81UF = (Hz < 59.0) ? PROT.t81UF + dt : 0; + PROT.t81OF = (Hz > 61.5) ? PROT.t81OF + dt : 0; + if (!S.Trip_81 && ((PROT.t81UF >= 0.5) || (PROT.t81OF >= 0.5))) { + logFlag('Trip_81', true); + try { + if (PROT.t81UF >= 0.5) logDebug('TRIP 81 (Underfrequency) LATCHED'); + if (PROT.t81OF >= 0.5) logDebug('TRIP 81 (Overfrequency) LATCHED'); + } catch(_) {} + } + }; + + window.updateProtectionGlows = function(){ + setGlow('Glow_Alarm_32', S.Alarm_32 === true); + setGlow('Glow_Alarm_55', S.Alarm_55 === true); + setGlow('Glow_Alarm_27', (S.Alarm_27 === true) || (S.Alarm_59 === true)); + setGlow('Glow_Alarm_81', S.Alarm_81 === true); + setGlow('Glow_Trip_32', S.Trip_32 === true); + setGlow('Glow_Trip_40', S.Trip_40 === true); + setGlow('Glow_Trip_27', S.Trip_27_59 === true); + setGlow('Glow_Trip_81', S.Trip_81 === true); + setGlow('Glow_Alarm_86G', S['86G_Trip_Var'] === true); + }; + + // Fallback looper + if (!window.__PROT_LOOP__){ + window.__PROT_LOOP__ = true; + (function loop(){ + try{ + if (typeof window.evaluateAlarms === 'function') window.evaluateAlarms(); + if (typeof window.evaluateTrips === 'function') window.evaluateTrips(); + if (typeof window.updateProtectionGlows === 'function') window.updateProtectionGlows(); + }catch(_){} + requestAnimationFrame(loop); + })(); + } +})(); + + + +/* ///////////// Section 5.Z1: Consolidated 86G Trip & Knob Management ///////////// */ +(function Auto86GTripAndKnobManager() { + const KNOB_ID = 'Knob_86G'; + const TRIP_ANGLE = -45; + const RESET_ANGLE = 0; + + const switchConfig = switches.find(s => s.knobId === KNOB_ID); + const originalType = switchConfig ? switchConfig.type : 'latching'; + + function rotateKnob(angle) { + const state = knobStates[KNOB_ID]; + const el = document.getElementById(KNOB_ID); + if (!state || !el) return; + const cx = state.centerX, cy = state.centerY; + state.currentAngle = angle; + if (prevAngles) prevAngles[KNOB_ID] = angle; + el.setAttribute('transform', `rotate(${angle} ${cx} ${cy})`); + } + + function loop() { + const s = window.SimState || window.state; + const k = knobStates[KNOB_ID]; + if (!s || !switchConfig || !k) { requestAnimationFrame(loop); return; } + + const isTripped = s['86G_Trip_Var']; + const anyProt = !!(s.Trip_32 || s.Trip_40 || s.Trip_27_59 || s.Trip_81); + + // Driveback and Auto-trip logic + if (anyProt && !isTripped) { + // Initiate driveback: set gate setpoint to 0 immediately. + // This will cause the actual gate position (Gate_Pos_Var) to ramp down. + Gate_Setpoint = 0; + gateRamp.active = false; // Ensure any conflicting start ramps are cancelled. + + // Check if the actual gate position has driven back far enough to trip. + if (s.Gate_Pos_Var <= 30) { + // If below the threshold, execute the 86G trip action. + handleAction('86G_TRIP'); + rotateKnob(TRIP_ANGLE); // Visually snap knob to trip position. + } + } + + // Behavior while tripped (unchanged from original) + if (isTripped) { + switchConfig.type = 'momentary'; + k.momentaryReturnAngle = TRIP_ANGLE; + switchConfig.maxAngle = -5; + + if (!k.isDragging && k.currentAngle !== TRIP_ANGLE) { + rotateKnob(TRIP_ANGLE); + } + } else { + // Reset restores latching behavior (unchanged from original) + switchConfig.type = originalType; + k.momentaryReturnAngle = null; + switchConfig.maxAngle = RESET_ANGLE; + } + + requestAnimationFrame(loop); + } + loop(); +})(); + +/* ///////////// Section 5.Z2 — Reset Button Debug Hook ///////////// */ +(function ResetButtonDebugHook(){ + function wire(){ + const btn = document.getElementById('Button_RESET'); // <-- corrected ID + if(!btn || btn.dataset._dbgHooked) return !!btn; + btn.dataset._dbgHooked = '1'; + + + // Use both pointerdown and click for safety + btn.addEventListener('pointerdown', () => { + + }, {passive:true}); + + btn.addEventListener('click', () => { + + }, {passive:true}); + + return true; + } + + if(!wire()){ + let tries = 0; + const iv = setInterval(() => { + if (wire() || ++tries > 50) clearInterval(iv); + }, 100); + } +})(); + +/* ///////////// Section 5.Z3 — Reset Handler (fixed) ///////////// */ +(function ResetHandler(){ + function doReset(){ + // Clear all auto trip latches (both internal vars and manager flags) + state['32_Trip_Var'] = false; + state['40_Trip_Var'] = false; + state['27_59_Trip_Var'] = false; + state['81_Trip_Var'] = false; + state['Trip_32'] = false; + state['Trip_40'] = false; + state['Trip_27_59'] = false; + state['Trip_81'] = false; + + // Clear lockout latch (86G) + state['86G_Trip_Var'] = false; + try { setFlag86(false); } catch(_){} + + // Clear online latch so logic can restart clean + state['GeneratorOnline'] = false; + + // Refresh lamps + try { updateProtectionGlows(); } catch(_){} + + // Debug + try { + + } catch(_){ + console.log('RESET: Trips (32/40/27-59/81) and 86G cleared; GeneratorOnline reset'); + } + } + + const btn = document.getElementById('Button_RESET'); + if(btn && !btn.dataset._resetHandler){ + btn.dataset._resetHandler = '1'; + btn.addEventListener('pointerdown', doReset, {passive:true}); + } +})(); + +/* ///////////// Section 5.Z4 — Master Stop Protections Inhibit (non-invasive) ///////////// */ +(function MasterStopProtectionsInhibit(){ + const S = window.SimState || window.state || (window.state = {}); + if (typeof S.MasterStopMask !== 'boolean') S.MasterStopMask = false; + + const origHandle = (typeof window.handleAction === 'function') ? window.handleAction : null; + window.handleAction = function(tag){ + try{ + if (tag === 'MASTER_STOP') { + if (!S.MasterStopMask){ + S.MasterStopMask = true; + try{ logDebug('Protections: Master Stop mask ENABLED'); }catch(_){} + } + } else if (tag === 'MASTER_START') { + if (S.MasterStopMask){ + S.MasterStopMask = false; + try{ logDebug('Protections: Master Stop mask CLEARED (start)'); }catch(_){} + } + } + }catch(_){} + return origHandle ? origHandle.apply(this, arguments) : undefined; + }; + + // Auto-clear when fully stopped + if (!window.__MS_MASK_LOOP__){ + window.__MS_MASK_LOOP__ = true; + (function loop(){ + try{ + if (S.MasterStopMask){ + const stopped = (!S['52G_Brk_Var']) && + (!S.Master_Started) && + ((+S.Gate_Pos_Var || 0) <= 0.5) && + ((+S.Gen_Freq_Var || 0) <= 0.2); + if (stopped){ + S.MasterStopMask = false; + try{ logDebug('Protections: Master Stop mask CLEARED (unit stopped)'); }catch(_){} + } + } + }catch(_){} + requestAnimationFrame(loop); + })(); + } +})(); + + +})(); + + +/* ///////////// Section 6 — RPM Text Binding (IIFE) ///////////// */ +(function () { + function getState() { return window.state || window.SimState || null; } + function calcRPM() { + const s = getState(); + if (!s) return null; + if (typeof s.Gen_RPM_Var === "number") return s.Gen_RPM_Var; + if (typeof s.Gen_Freq_Var === "number") return s.Gen_Freq_Var * 1.667; + return null; + } + function updateRPMText() { + const el = document.getElementById("Value_RPM"); + if (!el) return; + const rpm = calcRPM(); + if (rpm == null || isNaN(rpm)) return; + const v = Math.round(rpm); + if (el.textContent !== String(v)) el.textContent = String(v); + } + clearInterval(window.__rpm_bind_iv); + window.__rpm_bind_iv = setInterval(updateRPMText, 200); + document.addEventListener("DOMContentLoaded", updateRPMText); +})(); \ No newline at end of file diff --git a/Script.js b/Script.js deleted file mode 100644 index d83bf4f..0000000 --- a/Script.js +++ /dev/null @@ -1,1907 +0,0 @@ -/* ///////////// TABLE OF CONTENTS ///////////// - Section 1 — File Header & Overview - 1.A Feature summary banner - - Section 2 — Debug Harness - 2.A __setDebugWindow - 2.B logDebug - - Section 3 — Utilities - 3.A clamp / lerp / normAngleRad / deg2rad / rad2deg - - Section 4 — Switch Drag/Rotate Wiring (hit zones) - 4.A Config & globals (switches, sensitivity, knobStates) - 4.B Per-switch wiring & event handlers (setAngle/onMove/onUp) - 4.C angleOf helper - - Section 5 — Simulator Core (IIFE) - 5.A Rated constants (RATED) - 5.B State object (state) + exposure - 5.C Gate setpoint variable (Gate_Setpoint) - 5.D Master start/stop ramps (gateRamp/stopRamp) & kV ramp (kvRamp) - 5.E Voltage slew params (KV_* constants) - 5.F Angle watch thresholds & maps (THRESH/WATCH/prevAngles) - 5.G setFlag86 - 5.H handleAction (all operator actions) - 5.I watchAngles - 5.J updateGateSet (Knob_65) - 5.K updateVoltageSet (Knob_90) - 5.L updateSyncCheck (sync permissive) - 5.M updatePhysics (governor/AVR/power) - 5.N slewGenKV (kV tracker) - 5.O Syncroscope & Lamps - 5.O.1 SYNC struct - 5.O.2 parseRotateCenter - 5.O.3 initSyncUI - 5.O.4 updateSyncScopeAndLamps - 5.P Glow helpers (setGlow / setGlowWhite) - 5.Q updateGlows - 5.R updateGateGauge - 5.S Hz needle binding (IIFE) - 5.T updateKVgauge - 5.U fmtNoLeadingZeros - 5.V updateDigitals - 5.W Main tick loop (requestAnimationFrame) - 5.X Oscilloscope (Bus & Gen waveforms) - - Section 6 — RPM Text Binding (IIFE) -/////////////////////////////////////////// */ - -/* ///////////// Section 1 — File Header & Overview ///////////// */ -/* ///////////// Section 1.A Feature summary banner ///////////// */ -/* Sim_8.js - Complete build with: - - Switch drag wiring - - Start/Stop ramps + 86G trip/reset w/ flag color - - Gate nudge (Knob_65) - - Speed permissive - - Sync-check permissive (±3% V, ±0.15 Hz, <10°) - - Gen kV model + gauge (GenVolts_Rotation: 36°=0, 180°=13, 324°=15) - - Manual voltage (Knob_90); AVR only acts after 52G closes - - Digital readouts: Value_MW / Value_AMPS / Value_MVAR / Value_PowerFactor - - Hz needle + RPM text - - Status/permissive glows - - Syncroscope needle (SyncScope_Rotation) and TWO sync lamps - Lamps brightness ∝ |Vg∠θg − Vb∠θb|; both lamps identical behavior -*/ - -/* ///////////// Section 2 — Debug Harness ///////////// */ -/* ///////////// Section 2.A __setDebugWindow ///////////// */ -let __DEBUG_WIN = null; -function __setDebugWindow(win){ __DEBUG_WIN = win; } - -/* ///////////// Section 2.B logDebug ///////////// */ -function logDebug(message){ - try { - const msg = String(message); - try { console.log(msg); } catch(_) {} - const el = document.getElementById('Debug_Log'); - if (el){ - const line = document.createElement('div'); - line.textContent = msg; - const lower = msg.toLowerCase(); - let color; - if (lower.includes('trip')){ - if (lower.includes('cleared') || lower.includes('reset') || lower.includes('false') || lower.includes('inactive') || lower.includes('normal')){ - color = '#006400'; - } else { - color = 'red'; - } - } else if (lower.includes('overspeed')) { - color = 'red'; - } else if (lower.includes('alarm') || lower.includes('active') || lower.includes('inactive') || lower.includes('abnormal') || lower.includes('normal')){ - if (lower.includes('inactive') || (lower.includes('normal') && !lower.includes('abnormal')) || lower.includes('false') || lower.includes('cleared')){ - color = '#006400'; - } else { - color = '#b8860b'; - } - } - if (color) line.style.color = color; - el.appendChild(line); - // Always scroll to the bottom so newest messages are visible - // even when the log overflows the viewable area - try { el.scrollTop = el.scrollHeight; } catch (_) {} - } - if(__DEBUG_WIN && !__DEBUG_WIN.closed){ - __DEBUG_WIN.postMessage(msg, '*'); - } - } catch(_) {} -} - -/* ///////////// Section 3 — Utilities ///////////// */ -/* ///////////// Section 3.A clamp / lerp / normAngleRad / deg2rad / rad2deg ///////////// */ -function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); } -function lerp(a,b,t){ return a + (b - a) * t; } -function normAngleRad(a){ - // normalize to (-π, +π] - a = ((a + Math.PI) % (2*Math.PI) + 2*Math.PI) % (2*Math.PI) - Math.PI; - return a; -} -function deg2rad(d){ return d * Math.PI / 180; } -function rad2deg(r){ return r * 180 / Math.PI; } - -/* ///////////// Section 4 — Switch Drag/Rotate Wiring (hit zones) ///////////// */ -/* ///////////// Section 4.A Config & globals (switches, sensitivity, knobStates) ///////////// */ -const switches = [ - { parentId:'52G_Switch', knobId:'Knob_52G', upperHitId:'Hit_52G_Upper', lowerHitId:'Hit_52G_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, - { parentId:'41_Switch', knobId:'Knob_41', upperHitId:'Hit_41_Upper', lowerHitId:'Hit_41_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, - { parentId:'86G_Switch', knobId:'Knob_86G', upperHitId:'Hit_86G_Upper', lowerHitId:'Hit_86G_Lower', type:'latching', minAngle:-45, maxAngle:0 }, - { parentId:'AVR_Switch', knobId:'Knob_AVR', upperHitId:'Hit_AVR_Upper', lowerHitId:'Hit_AVR_Lower', type:'latching', minAngle:-45, maxAngle:45 }, - { parentId:'Sync_Switch', knobId:'Knob_Sync', upperHitId:'Hit_Sync_Upper', lowerHitId:'Hit_Sync_Lower', type:'latching', minAngle:-45, maxAngle:45 }, - { parentId:'Master_Switch',knobId:'Knob_Master',upperHitId:'Hit_Master_Upper',lowerHitId:'Hit_Master_Lower',type:'momentary', minAngle:-45, maxAngle:45 }, - { parentId:'65_Switch', knobId:'Knob_65', upperHitId:'Hit_65_Upper', lowerHitId:'Hit_65_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, - { parentId:'90_Switch', knobId:'Knob_90', upperHitId:'Hit_90_Upper', lowerHitId:'Hit_90_Lower', type:'momentary', minAngle:-45, maxAngle:45 }, -]; -const sensitivity = 0.5; // deg per px -const knobStates = {}; - -/* ///////////// Section 4.B Per-switch wiring & event handlers (setAngle/onMove/onUp) ///////////// */ -switches.forEach(cfg => { - const svg = document.getElementById(cfg.parentId); - const knob = document.getElementById(cfg.knobId); - const hitU = document.getElementById(cfg.upperHitId); - const hitL = document.getElementById(cfg.lowerHitId); - if (!svg || !knob || !hitU || !hitL) return; - - const bb = svg.getBBox(); - const cx = bb.x + bb.width/2; - const cy = bb.y + bb.height/2; - - // AVR knob starts at -45° (OFF); Sync knob at -45°; others at 0° - const initAngle = - (cfg.knobId === 'Knob_AVR') ? -45 : - (cfg.knobId === 'Knob_Sync') ? -45 : 0; - - knobStates[cfg.knobId] = { - isDragging:false, startX:0, currentAngle:initAngle, - centerX:cx, centerY:cy, minAngle:cfg.minAngle, maxAngle:cfg.maxAngle, type:cfg.type - }; - - // Apply initial visual angle - knob.setAttribute('transform', `rotate(${initAngle} ${cx} ${cy})`); - - function setAngle(knobId, ang){ - knobStates[knobId].currentAngle = ang; - knob.setAttribute('transform', `rotate(${ang} ${cx} ${cy})`); - } - - hitU.addEventListener('mousedown', (e)=>{ - e.preventDefault(); - knobStates[cfg.knobId].isDragging = true; - knobStates[cfg.knobId].startX = e.clientX; - document.addEventListener('mousemove', onMoveU); - document.addEventListener('mouseup', onUp); - }); - function onMoveU(e){ - if(!knobStates[cfg.knobId].isDragging) return; - const dx = e.clientX - knobStates[cfg.knobId].startX; - const ang = clamp(dx * sensitivity, cfg.minAngle, cfg.maxAngle); - setAngle(cfg.knobId, ang); - } - - hitL.addEventListener('mousedown', (e)=>{ - e.preventDefault(); - knobStates[cfg.knobId].isDragging = true; - knobStates[cfg.knobId].startX = e.clientX; - document.addEventListener('mousemove', onMoveL); - document.addEventListener('mouseup', onUp); - }); - function onMoveL(e){ - if(!knobStates[cfg.knobId].isDragging) return; - const dx = e.clientX - knobStates[cfg.knobId].startX; - const ang = clamp(-dx * sensitivity, cfg.minAngle, cfg.maxAngle); - setAngle(cfg.knobId, ang); - } - - function onUp(){ - if(!knobStates[cfg.knobId].isDragging) return; - knobStates[cfg.knobId].isDragging = false; - - // Momentary returns to defined angle (default 0); - // Latching snaps to nearest extreme - if (cfg.type === 'momentary') { - const returnAngle = (knobStates[cfg.knobId].momentaryReturnAngle != null) - ? knobStates[cfg.knobId].momentaryReturnAngle - : 0; - setAngle(cfg.knobId, returnAngle); - } else { - const curr = knobStates[cfg.knobId].currentAngle; - const mid = (cfg.minAngle + cfg.maxAngle) / 2; - setAngle(cfg.knobId, curr >= mid ? cfg.maxAngle : cfg.minAngle); - } - - document.removeEventListener('mousemove', onMoveU); - document.removeEventListener('mousemove', onMoveL); - document.removeEventListener('mouseup', onUp); - } -}); - -/* ///////////// Section 4.C angleOf helper ///////////// */ -function angleOf(id){ - try{ - if (knobStates[id] && typeof knobStates[id].currentAngle === 'number') return knobStates[id].currentAngle; - }catch(_){} - const el = document.getElementById(id); - if(!el) return 0; - const t = el.getAttribute('transform') || ''; - const m = t.match(/rotate\(([-\d.]+)/); - return m ? parseFloat(m[1]) : 0; -} - -/* ///////////// Section 5 — Simulator Core (IIFE) ///////////// */ -(function(){ - /* ///////////// Section 5.A Rated constants (RATED) ///////////// */ - const RATED = { - KV_LL: 13.8, // kV line-line nominal - MVA: 25, // MVA rating - MW: 23.5, // continuous MW - MVAR_LAG_MAX: 15.5, // +Q - MVAR_LEAD_MAX: 19.4, // -Q - AMPS: 1046.9 // at 13.8kV - }; - - // Inertia: higher values slow both acceleration and deceleration when off-grid. - // Tune by changing the numeric constant below—no UI controls adjust this value. - const INERTIA_TIME_CONST_S = 1.5; - - /* ///////////// Section 5.B State object (state) + exposure ///////////// */ - const state = { - Master_Started:false, - AVR_On:false, - Sync_On:false, - - // Latches TRUE when 52G closes; does NOT auto-unlatch on open - GeneratorOnline:false, - - '41_Brk_Var':false, // field breaker - '52G_Brk_Var':false, // gen breaker (FALSE=open, TRUE=closed) - '86G_Trip_Var':false, - - Gate_Pos_Var:0, // % - Gen_Freq_Var:0, // Hz - Gen_RPM_Var:0, // calc - - Speed_Perm_Var:false, - SyncCheck_Perm_Var:false, - - // Bus references - Bus_Freq_Hz:60, - Bus_Voltage_kV:13.8, - - // Voltage model - Gen_kV_Var:0, // actual terminal kV “inside the 52G” - Gen_kV_SP:13.5, // operator/AVR setpoint (kV) - - // Governor/gate dynamics - // Power model - MW:0, - MVAR:0, - AMPS:0, - PF:0 - }; - try{ window.SimState = state; }catch(_){} - - - /* ///////////// Section 5.C Gate setpoint variable (Gate_Setpoint) ///////////// */ - let Gate_Setpoint = 0; - - /* ///////////// Section 5.D Master start/stop ramps (gateRamp/stopRamp) ///////////// */ - const gateRamp = { active:false, from:0, to:19.67, dur:3000, t0:0 }; - const stopRamp = { active:false, from:0, to:0, dur:0, t0:0 }; // ramp-to-zero on STOP - - // Shutdown ramp durations (ms) - // Normal: Master STOP; Trip: protective trip or 86G lockout - const STOP_RAMP_NORMAL_MS = 2000; - const STOP_RAMP_TRIP_MS = 500; - const KV_RAMP_MS = 3000; - - const kvRamp = { active:false, from:0, to:0, dur:KV_RAMP_MS, t0:0 }; // kV ramp on field breaker - - /* ///////////// Section 5.E Voltage slew params (KV_* constants) ///////////// */ - const KV_SLEW_MANUAL = 2; // kV/s tracking rate to SP (manual) - const KV_SLEW_AUTO = 1.2; // kV/s when AVR is acting - const KV_MIN = 0.0, KV_MAX = 16.0; - - /* Frequency tuning (single-mode) */ - const FREQ_GATE_THRESH_PCT = 20; // gate % breakpoint for frequency - const FREQ_GATE_LOW_HZ_PER_PCT = 3; // Hz per % gate below threshold - const FREQ_GATE_HIGH_HZ_PER_PCT = 0.375; // Hz per % gate above threshold - const FREQ_GATE_HIGH_INTERCEPT_HZ = 52.5; // offset for high range - // AVR line-drop compensation (disabled if 0) - const AVR_LDC_PU = 0.00; - - // Power mapping: physical gate needed for ~0 MW when paralleled - const NO_LOAD_GATE_PCT = 20; // set near your sync gate (e.g., 18–20) - const REV_PWR_LIMIT_MW = -5; // cap reverse power (negative) - - // Manual-close PF/MW targets (used when AVR is OFF, at 52G close) - const CLOSE_REV_PWR_TARGET_MW = -0.01; - const CLOSE_PF_TARGET = -0.95; - - // Small manual kV bias at close (optional) - const MANUAL_CLOSE_V_BIAS_KV = 12.5; - - // kV droop in manual mode at full load (approx) - const MANUAL_KV_DROOP = 0.4; - - // Reactive gain shaping (keeps vars small near zero load) - const Q_GAIN_MIN = 2.0; // MVAR per kV near zero-load - const Q_GAIN_MAX = 30.0; // MVAR per kV at high load - const Q_GAIN_SHAPE_N = 1.0; // 1=linear - - // ---------- Loss Of Excitation (41 OPEN while 52G CLOSED) tunables ---------- - const LOE_REV_PWR_MW = -0.4; // small reverse MW on field loss - const LOE_Q_IMPORT_MVAR = 15.0; // inductive vars imported on field loss (+ = lagging) - const LOE_SETTLE_MS = 250; // time constant to settle MW/Q to targets - - - - - - - - - /* ///////////// Section 5.F Angle watch thresholds & maps (THRESH/WATCH/prevAngles) ///////////// */ - const THRESH = { up:20, down:-20 }; - const WATCH = [ - { knobIds:['Knob_Master'], upper:'MASTER_START', lower:'MASTER_STOP' }, - { knobIds:['Knob_AVR'], upper:'AVR_ON', lower:'AVR_OFF' }, - { knobIds:['Knob_Sync'], upper:'SYNC_ON', lower:'SYNC_OFF' }, - { knobIds:['Knob_41'], upper:'41_CLOSE', lower:'41_OPEN' }, - { knobIds:['Knob_52G'], upper:'52G_CLOSE', lower:'52G_OPEN' }, - { knobIds:['Knob_86G','Knob_86'], special:'86G' }, - ]; - const prevAngles = Object.create(null); - - /* ///////////// Section 5.G setFlag86 ///////////// */ - function setFlag86(){ - try{ - const el = document.getElementById('Flag_86G'); - if (!el) return; - - const k = (typeof knobStates !== 'undefined') ? knobStates['Knob_86G'] : null; - const angle = (k && typeof k.currentAngle === 'number') ? k.currentAngle : 0; - - // < -44° = dark orange; > -1° = default color - el.style.fill = (angle < -44) ? '#CD5A00' : ''; - }catch(_){} - } - - /* ///////////// Section 5.H handleAction (all operator actions) ///////////// */ - function handleAction(tag){ - switch(tag){ - /* ---- Syncroscope switch ---- */ - case 'SYNC_ON': - if (!state.Sync_On){ state.Sync_On = true; try { logDebug('SYNCROSCOPE: ON'); } catch(_){} } - break; - case 'SYNC_OFF': - if (state.Sync_On){ state.Sync_On = false; try { logDebug('SYNCROSCOPE: OFF'); } catch(_){} } - break; - - /* ---- AVR switch ---- */ - case 'AVR_ON': - if (!state.AVR_On){ - state.AVR_On = true; - try { - const sp = (+state.Gen_kV_SP || 0).toFixed(2); - const kv = (+state.Gen_kV_Var || 0).toFixed(2); - logDebug(`AVR: AUTO (SP: ${sp} kV, V: ${kv} kV)`); - } catch(_){} - } - break; - case 'AVR_OFF': - if (state.AVR_On){ - state.AVR_On = false; - try { - const sp = (+state.Gen_kV_SP || 0).toFixed(2); - const kv = (+state.Gen_kV_Var || 0).toFixed(2); - logDebug(`AVR: MANUAL (SP: ${sp} kV, V: ${kv} kV)`); - } catch(_){} - } - break; - - /* ---- Master ---- */ - case 'MASTER_START': { - // 86G knob permissive + not tripped - const k86 = (typeof knobStates !== 'undefined') ? knobStates['Knob_86G'] : null; - const ang86 = (k86 && typeof k86.currentAngle === 'number') ? k86.currentAngle : 0; - if (ang86 <= -1){ try{ logDebug('Master: BLOCKED (86G Permissive)'); }catch(_){ } break; } - if (state['86G_Trip_Var']){ try{ logDebug('Master: BLOCKED (86G: Trip)'); }catch(_){ } break; } - - // prevent double staging - if (state.__prestartBusy) break; - state.__prestartBusy = true; - if (!Array.isArray(state.__prestartTimers)) state.__prestartTimers = []; - const T = state.__prestartTimers; - const at = (ms, fn) => T.push(setTimeout(fn, ms)); - - try{ logDebug('Master: START'); }catch(_){} - - // +1.0s — permissives - at(1000, () => { - const perm52 = !state['52G_Brk_Var']; - if (perm52) try{ logDebug('52G Permissive OK'); }catch(_){} - const k86s = knobStates?.['Knob_86G']; - const ang86s = (k86s && typeof k86s.currentAngle === 'number') ? k86s.currentAngle : 0; - if (ang86s > -1) try{ logDebug('86G Permissive OK'); }catch(_){} - }); - - // +2.0s — lift pump - at(2000, () => { try{ logDebug('Lift Pump On'); }catch(_){}; }); - - // +4.0s — pressure ok - at(4000, () => { try{ logDebug('Lift Pump Pressure OK'); }catch(_){}; }); - - // +5.0s — brakes release - at(5000, () => { try{ logDebug('Brakes Released'); }catch(_){}; }); - - // +5.1s — handoff to normal start - at(5100, () => { - const k86f = knobStates?.['Knob_86G']; - const ang86f = (k86f && typeof k86f.currentAngle === 'number') ? k86f.currentAngle : 0; - const ok86 = (ang86f > -1) && !state['86G_Trip_Var']; - if (!ok86){ state.__prestartBusy = false; return; } - - if (!state.Master_Started){ - state.Master_Started = true; - stopRamp.active = false; - gateRamp.active = true; - gateRamp.from = (typeof Gate_Setpoint === 'number') ? Gate_Setpoint : 0; - gateRamp.to = 19.67; - gateRamp.dur = 3000; - gateRamp.t0 = performance.now(); - } - - state.__prestartBusy = false; - while (T.length) clearTimeout(T.pop()); - }); - - break; - } - - case 'MASTER_STOP': { - try{ logDebug('Master: STOP'); }catch(_){} - // cancel any staged start sequence - state.__prestartBusy = false; - if (Array.isArray(state.__prestartTimers)) { - while (state.__prestartTimers.length) clearTimeout(state.__prestartTimers.pop()); - } - // start STOP ramp - gateRamp.active = false; - stopRamp.active = true; - stopRamp.from = (typeof Gate_Setpoint === 'number') ? Gate_Setpoint : 0; - stopRamp.to = 0; - stopRamp.dur = STOP_RAMP_NORMAL_MS; - stopRamp.t0 = performance.now(); - break; - } - - /* ---- 41 Field breaker ---- */ - case '41_CLOSE': - if(!state['41_Brk_Var']){ - if (state.Speed_Perm_Var){ - state['41_Brk_Var'] = true; - try{ logDebug('Field Breaker: CLOSED'); }catch(_){} - if (state['52G_Brk_Var'] && state.AVR_On){ - state.Gen_kV_SP = 13.8; - } else { - state.Gen_kV_SP = 13.34; - } - kvRamp.active = true; - kvRamp.from = state.Gen_kV_Var; - kvRamp.to = state.Gen_kV_SP; - kvRamp.dur = KV_RAMP_MS; - kvRamp.t0 = performance.now(); - } else { - try{ logDebug('41: BLOCKED'); }catch(_){} - } - } - break; - - case '41_OPEN': - if(state['41_Brk_Var']){ - state['41_Brk_Var'] = false; - try{ logDebug('Field Breaker: OPEN'); }catch(_){} - kvRamp.active = true; - kvRamp.from = state.Gen_kV_Var; - kvRamp.to = 0; - kvRamp.dur = KV_RAMP_MS; - kvRamp.t0 = performance.now(); - } - break; - - /* ---- 52G Generator breaker ---- */ - case '52G_CLOSE': - if(!state['52G_Brk_Var']){ - if (state.SyncCheck_Perm_Var){ - state['52G_Brk_Var'] = true; - try{ logDebug('52G: CLOSED'); }catch(_){} - - // Latch "online" (no auto-unlatch) - if (!state.GeneratorOnline){ - state.GeneratorOnline = true; - try { logDebug('Unit Online'); } catch(_){} - } - - // No-load calibration at close - (function(){ - const slope = 100 / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)); - const MWpu = (CLOSE_REV_PWR_TARGET_MW) / (RATED.MW || 1); - const effNeededPct = (MWpu * 100) / slope; // negative for reverse - state.NoLoadGateCal = clamp(state.Gate_Pos_Var - effNeededPct, 0, 100); - })(); - - if (state.AVR_On){ - state.Gen_kV_SP = 13.8; - state.Gen_kV_Var = state.Gen_kV_SP; - } else { - // manual bias at close - const S = (RATED.MVA || 1); - const pAbs = Math.abs(CLOSE_REV_PWR_TARGET_MW); - const qAbs = Math.sqrt(Math.max(0, S*S - pAbs*pAbs)); - const qTarget = (CLOSE_PF_TARGET < 0 ? -qAbs : qAbs); - const qGainClose = Q_GAIN_MIN; - let dvBias = (qGainClose > 1e-6) ? (qTarget / qGainClose) : 0; - dvBias = clamp(dvBias + MANUAL_CLOSE_V_BIAS_KV, -0.5, 0.5); - state.Gen_kV_SP = Math.max(0, (+state.Gen_kV_SP || 0) + dvBias); - state.Gen_kV_Var = state.Gen_kV_SP; - } - } - } - break; - - case '52G_OPEN': - if(state['52G_Brk_Var']){ - state['52G_Brk_Var'] = false; - delete state.NoLoadGateCal; - try{ logDebug('52G: OPEN'); }catch(_){} - } - break; - - /* ---- 86G Lockout ---- */ - case '86G_TRIP': - if(!state['86G_Trip_Var']){ - state['86G_Trip_Var'] = true; - setFlag86(true); - if(state['41_Brk_Var']){ state['41_Brk_Var'] = false; try{ logDebug('Field Breaker: Trip'); }catch(_){} } - if(state['52G_Brk_Var']){ state['52G_Brk_Var'] = false; try{ logDebug('Generator Breaker: Trip'); }catch(_){} } - gateRamp.active = false; - stopRamp.active = true; - stopRamp.from = (typeof Gate_Setpoint === 'number') ? Gate_Setpoint : 0; - stopRamp.to = 0; - stopRamp.dur = STOP_RAMP_TRIP_MS; - stopRamp.t0 = performance.now(); - try{ logDebug('86G: TRIP'); }catch(_){} - } - break; - - case '86G_RESET': - if(state['86G_Trip_Var']){ - state['86G_Trip_Var'] = false; - setFlag86(false); - try{ logDebug('86G: RESET'); }catch(_){} - } - break; - } - } - - - - /* ///////////// Section 5.H.1 Enforce 86G Permissive on MASTER_START (add-on) ///////////// */ - - (function Enforce86GPermOnStart(){ - const S = window.SimState || window.state || (window.state = {}); - const _old = window.handleAction; - - function readKnob86Angle(){ - // preferred: knobStates cache - const ks = (window.knobStates && window.knobStates['Knob_86G']) || null; - if (ks && typeof ks.currentAngle === 'number') return ks.currentAngle; - - // fallback: parse DOM transform - const el = document.getElementById('Knob_86G'); - if (el){ - const tr = el.getAttribute('transform') || ''; - const m = tr.match(/rotate\((-?\d+(?:\.\d+)?)/i); - if (m) return parseFloat(m[1]); - } - return 0; // default safe - } - - function get86GPermissive(){ - const ang = readKnob86Angle(); - const perm = (ang > -1); // same threshold used for Glow_Perm_86G - S['86G_Perm_Var'] = perm; // keep a state bit for reuse - return perm; - } - - window.handleAction = function(tag){ - if (tag === 'MASTER_START'){ - // Block if 86G knob not in NORMAL or lockout is tripped - const perm86 = get86GPermissive(); - if (!perm86){ - try{ logDebug('Master: BLOCKED (86G Permissive)'); }catch(_){} - return; - } - if (S['86G_Trip_Var'] === true){ - try{ logDebug('Master: BLOCKED (86G Tripped)'); }catch(_){} - return; - } - } - return _old ? _old.apply(this, arguments) : undefined; - }; - })(); - - - /* ///////////// Section 5.H.2 AVR takeover harmonizer (non-invasive) ///////////// */ - (function AVRTakeoverHarmonizer(){ - if (typeof window === 'undefined') return; - const origHandle = (typeof window.handleAction === 'function') ? window.handleAction : null; - if (!origHandle) return; - - window.handleAction = function(tag){ - // Pre-transition context - const wasAVR = !!(state && state.AVR_On); - - // On enabling AVR, restore previously stored SP or freeze to present kV - if (tag === 'AVR_ON' && !wasAVR && state) { - if (typeof state.__avrStoredSP === 'number') { - state.Gen_kV_SP = clamp(state.__avrStoredSP, KV_MIN, KV_MAX); - } else { - const kv = +state.Gen_kV_Var || 0; - if (typeof clamp === 'function') { - state.Gen_kV_SP = clamp(kv, KV_MIN, KV_MAX); - } else { - state.Gen_kV_SP = Math.min(KV_MAX, Math.max(KV_MIN, kv)); - } - } - } - - // On disabling AVR, store SP and freeze setpoint at present kV - if (tag === 'AVR_OFF' && wasAVR && state) { - state.__avrStoredSP = state.Gen_kV_SP; - const kv = Math.max(0, +state.Gen_kV_Var || 0); - state.Gen_kV_SP = kv; // freeze setpoint to present kV - state.Gen_kV_Var = kv; - try { - const kvStr = kv.toFixed(2); - logDebug(`AVR transition to MANUAL: V ${kvStr} kV`); - } catch(_){} - } - - // Delegate to original action handler - return origHandle.call(this, tag); - }; - })(); - - - - - /* ///////////// Section 5.I watchAngles ///////////// */ - function watchAngles(){ - for(const w of WATCH){ - let ang = null; - for(const id of w.knobIds){ - const a = angleOf(id); - if(a !== 0 || document.getElementById(id)){ ang = a; break; } - } - if(ang === null) continue; - const key = w.knobIds[0]; - const prev = prevAngles[key] ?? ang; - - if (w.special === '86G'){ - const T = -1; // degrees - const hadPrev = Object.prototype.hasOwnProperty.call(prevAngles, key); - - if (!hadPrev){ - // First run: align state to current knob position - if (ang <= T) { handleAction('86G_TRIP'); } - else { handleAction('86G_RESET'); } - } else { - // Subsequent frames: edge-detect crossings - if (prev > T && ang <= T) handleAction('86G_TRIP'); - if (prev <= T && ang > T) handleAction('86G_RESET'); - } - } else { - const S = window.SimState || window.state || (window.state = {}); - if (prev < THRESH.up && ang >= THRESH.up) { - if (w.upper === 'MASTER_START' && S.MasterStopMask) { - S.MasterStopMask = false; - } - handleAction(w.upper); - } - if (prev > THRESH.down && ang <= THRESH.down) { - if (w.lower === 'MASTER_STOP' && !S.MasterStopMask) { - S.MasterStopMask = true; - } - handleAction(w.lower); - } - } - prevAngles[key] = ang; - } - } - - /* ///////////// Section 5.J updateGateSet (Knob_65) ///////////// */ - function updateGateSet(){ - const NUDGE_THRESH = 20; // degrees - const NUDGE_RATE_OPEN = 0.125; // %/s when 52G OPEN - const NUDGE_RATE_CLOSED = 10; // %/s when 52G CLOSED - const NUDGE_RATE = state['52G_Brk_Var'] ? NUDGE_RATE_CLOSED : NUDGE_RATE_OPEN; - - const now = performance.now(); - if (typeof updateGateSet._tPrev !== 'number') updateGateSet._tPrev = now; - const dt = Math.min(150, now - updateGateSet._tPrev) / 1000; - updateGateSet._tPrev = now; - - const a65 = angleOf('Knob_65') || 0; - - if (a65 >= NUDGE_THRESH){ - Gate_Setpoint = Math.min(100, Gate_Setpoint + NUDGE_RATE * dt); - if (updateGateSet._lastLog == null || Math.abs(Gate_Setpoint - updateGateSet._lastLog) >= 0.5){ - updateGateSet._lastLog = Gate_Setpoint; - } - } else if (a65 <= -NUDGE_THRESH){ - Gate_Setpoint = Math.max(0, Gate_Setpoint - NUDGE_RATE * dt); - if (updateGateSet._lastLog == null || Math.abs(Gate_Setpoint - updateGateSet._lastLog) >= 0.5){ - updateGateSet._lastLog = Gate_Setpoint; - } - } - } - - /* ///////////// Section 5.K updateVoltageSet (Knob_90) ///////////// */ - function updateVoltageSet(){ - const TH = 20; // deg threshold - const now = performance.now(); - if (typeof updateVoltageSet._tPrev !== 'number') updateVoltageSet._tPrev = now; - const dt = Math.min(150, now - updateVoltageSet._tPrev) / 1000; - updateVoltageSet._tPrev = now; - - const a90 = angleOf('Knob_90') || 0; - const NU = 0.01; // sensitivity - const RATE = NU * (KV_MAX - KV_MIN); // kV/s equivalent - - if (a90 >= TH){ - state.Gen_kV_SP = state.AVR_On - ? clamp(state.Gen_kV_SP + RATE*dt, KV_MIN, KV_MAX) - : Math.max(0, state.Gen_kV_SP + RATE*dt); // no upper limit when AVR OFF - } else if (a90 <= -TH){ - state.Gen_kV_SP = state.AVR_On - ? clamp(state.Gen_kV_SP - RATE*dt, KV_MIN, KV_MAX) - : Math.max(0, state.Gen_kV_SP - RATE*dt); // allow down to 0 only - } - } - - - /* ///////////// Section 5.L updateSyncCheck (sync permissive) ///////////// */ - function updateSyncCheck(){ - const V_TOL_FRAC = 0.03; // ±3% - const F_TOL_HZ = 0.15; // ±0.15 Hz - const PHASE_DEG = 10.0; // ±10° - - const snap = PhaseTracker.snap; - const vb = snap ? snap.vb : (+state.Bus_Voltage_kV || 13.8); - const vg = snap ? snap.vg : (+state.Gen_kV_Var || 0); - const fb = snap ? snap.fb : (+state.Bus_Freq_Hz || 60); - const fg = snap ? snap.fg : (+state.Gen_Freq_Var || 0); - const ddeg = snap ? snap.dphiDeg : PhaseTracker.deltaDeg(); - - const vOK = Math.abs(vg - vb) <= V_TOL_FRAC * vb; - const fOK = Math.abs(fg - fb) <= F_TOL_HZ; - const pOK = Math.abs(ddeg) <= PHASE_DEG; - - const ok = !!(vOK && fOK && pOK); - if (ok !== state.SyncCheck_Perm_Var){ - state.SyncCheck_Perm_Var = ok; - - } - } - - /* ///////////// Section 5.M updatePhysics (governor/AVR/power) ///////////// */ - function updatePhysics(){ - if (!state.Master_Started) { - gateRamp.active = false; - stopRamp.active = false; - - state.Gen_Freq_Var = 0; - state.Gen_RPM_Var = 0; - - const targetKV = state['41_Brk_Var'] ? state.Gen_kV_SP : 0; - slewGenKV(targetKV, KV_SLEW_MANUAL); - - if (state.Speed_Perm_Var !== false) { - state.Speed_Perm_Var = false; - } - - // reset run latch - updatePhysics._wasRunning = false; - - state.MW = 0; state.MVAR = 0; state.AMPS = 0; state.PF = 0; - return; - } - - // Gate setpoint ramps - if (gateRamp.active){ - const p = clamp((performance.now() - gateRamp.t0) / gateRamp.dur, 0, 1); - Gate_Setpoint = gateRamp.from + (gateRamp.to - gateRamp.from) * p; - if (p >= 1){ - gateRamp.active = false; - } - } else if (stopRamp.active){ - const p = clamp((performance.now() - stopRamp.t0) / stopRamp.dur, 0, 1); - Gate_Setpoint = stopRamp.from + (stopRamp.to - stopRamp.from) * p; - if (p >= 1){ - stopRamp.active = false; - Gate_Setpoint = 0; - // do NOT snap gates/freq or clear Master_Started here - } - } - - // Governor: actual gate follows setpoint. Rates differ for normal vs. trip shutdowns. - const GATE_SLEW = { - NORMAL: 6 / 1000, // %/ms (≈20 %/s) — normal shutdown - TRIP: 8 / 1000 // %/ms (≈80 %/s) — trip - }; - - const isTripSlew = !!( - state['86G_Trip_Var'] || - state.Trip_32 || state.Trip_40 || state.Trip_27_59 || state.Trip_81 - ); - const rate = isTripSlew ? GATE_SLEW.TRIP : GATE_SLEW.NORMAL; - - const now = performance.now(); - if (typeof updatePhysics._tPrev !== 'number') updatePhysics._tPrev = now; - const dt = Math.min(100, now - updatePhysics._tPrev); - updatePhysics._tPrev = now; - - const maxStep = rate * dt; - const err = Gate_Setpoint - state.Gate_Pos_Var; - if (Math.abs(err) > maxStep){ - state.Gate_Pos_Var += Math.sign(err) * maxStep; - } else { - state.Gate_Pos_Var = Gate_Setpoint; - } - - // During a Master Stop, automatically open breakers once gates fall low - if (state.MasterStopMask && state.Gate_Pos_Var <= FREQ_GATE_THRESH_PCT) { - if (state['52G_Brk_Var']) handleAction('52G_OPEN'); - if (state['41_Brk_Var']) handleAction('41_OPEN'); - } - - /// Frequency (single-owner slew): on-grid=60; off-grid ramps toward gate-implied speed using inertia - { - const onGrid = !!state['52G_Brk_Var']; - let raw; - if (onGrid) { - raw = 60; - } else { - const gate = state.Gate_Pos_Var; - raw = (gate <= FREQ_GATE_THRESH_PCT) - ? FREQ_GATE_LOW_HZ_PER_PCT * gate - : (FREQ_GATE_HIGH_HZ_PER_PCT * gate + FREQ_GATE_HIGH_INTERCEPT_HZ); - } - const curr = +state.Gen_Freq_Var || 0; - const dt_s = Math.max(0, dt) / 1000; - - let next; - if (onGrid) { - next = raw; - } else { - const inertia = INERTIA_TIME_CONST_S; - const step = (raw - curr) * (dt_s / inertia); - next = curr + step; - // Avoid overshoot when the step would cross the target - if (Math.sign(raw - curr) !== Math.sign(raw - next)) { - next = raw; - } - } - - state.Gen_Freq_Var = clamp(next, 0, 94); - state.Gen_RPM_Var = state.Gen_Freq_Var * 1.667; - - // Log major stopping events based on frequency thresholds - if (!state['52G_Brk_Var'] && state.Master_Started && curr > state.Gen_Freq_Var) { - if (!updatePhysics._liftPumpLogged && curr >= 40 && state.Gen_Freq_Var < 40) { - try { logDebug('Lift Pump On'); } catch (_) {} - updatePhysics._liftPumpLogged = true; - } - if (!updatePhysics._brakesLogged && curr >= 20 && state.Gen_Freq_Var < 20) { - try { logDebug('Brakes Applied'); } catch (_) {} - updatePhysics._brakesLogged = true; - } - } else { - updatePhysics._liftPumpLogged = false; - updatePhysics._brakesLogged = false; - } - } - // Mark as having run (prevents immediate "Unit Stopped" right after Master Start) - { - if (state.Master_Started && (state.Gate_Pos_Var > 0.5 || state.Gen_Freq_Var > 0.2)) { - updatePhysics._wasRunning = true; - } - } - - // Latch "Unit Stopped" only when we are actually stopping AND gates ~0 AND frequency ~0 (off-grid) - { - const CLOSE_LATCH_EPS = 0.5; // % - const FREQ_LATCH_EPS = 0.2; // Hz - const stoppingIntent = stopRamp.active || state['86G_Trip_Var'] || !!updatePhysics._wasRunning; - - if (!state['52G_Brk_Var'] && - state.Master_Started && - stoppingIntent && - state.Gate_Pos_Var <= CLOSE_LATCH_EPS && - state.Gen_Freq_Var <= FREQ_LATCH_EPS) { - state.Gate_Pos_Var = 0; - state.Master_Started = false; - stopRamp.active = false; - updatePhysics._wasRunning = false; - // Mark generator offline so protections don't evaluate after a normal stop - state.GeneratorOnline = false; - logDebug('Unit Stopped'); - } - } - - // Speed Permissive toggle - const spNext = !!(state.Master_Started && (state.Gate_Pos_Var > 0) && (state.Gen_RPM_Var > 50)); - if (state.Speed_Perm_Var !== spNext) { - state.Speed_Perm_Var = spNext; - } - - // AVR & kV tracking - const Vbus = state.Bus_Voltage_kV || 13.8; - if (state['52G_Brk_Var']) { - if (state.AVR_On){ - let kvTargetSP = state.Gen_kV_SP; - slewGenKV(kvTargetSP, KV_SLEW_AUTO); - } else { - // MANUAL: allow small voltage droop with load - const noLoad = (typeof state.NoLoadGateCal === 'number') ? state.NoLoadGateCal : NO_LOAD_GATE_PCT; - const effGatePU = clamp( - (state.Gate_Pos_Var - noLoad) / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)), - 0, 1 - ); - const kvTarget = state.Gen_kV_SP - MANUAL_KV_DROOP * effGatePU; - slewGenKV(kvTarget, KV_SLEW_MANUAL); - } - } else { - // Not paralleled: track SP if field on, else decay to 0 - const tgt = state['41_Brk_Var'] ? state.Gen_kV_SP : 0; - slewGenKV(tgt, KV_SLEW_MANUAL); - } - - // Power model (with no-load gate offset; does NOT move the gates) - let MW = 0; - if (state['52G_Brk_Var']){ - const noLoad = (typeof state.NoLoadGateCal === 'number') ? state.NoLoadGateCal : NO_LOAD_GATE_PCT; - const effGate = state.Gate_Pos_Var - noLoad; // can be negative - const slope = 100 / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)); // keep 100% gate => rated MW - let MW_pu = (effGate * slope) / 100; // per-unit MW - const min_pu = (REV_PWR_LIMIT_MW) / (RATED.MW || 1); - MW_pu = clamp(MW_pu, min_pu, 1); - MW = MW_pu * RATED.MW; - } - - // Reactive power: scale gain with effective gate so MVAR is small at close - let Q = 0; - if (state['52G_Brk_Var']){ - const dv = (state.Gen_kV_Var - Vbus); // kV - const noLoad = (typeof state.NoLoadGateCal === 'number') ? state.NoLoadGateCal : NO_LOAD_GATE_PCT; - const effGatePU = clamp( - (state.Gate_Pos_Var - noLoad) / Math.max(1e-3, (100 - NO_LOAD_GATE_PCT)), - 0, 1 - ); - const qGain = Q_GAIN_MIN + (Q_GAIN_MAX - Q_GAIN_MIN) * Math.pow(effGatePU, Q_GAIN_SHAPE_N); - Q = clamp(dv * qGain, -RATED.MVAR_LEAD_MAX, RATED.MVAR_LAG_MAX); - } - - // -------- Loss Of Excitation override (52G CLOSED & 41 OPEN) ---------- - if (state['52G_Brk_Var'] && !state['41_Brk_Var']){ - const t = performance.now(); - if (typeof updatePhysics._loeT0 !== 'number') updatePhysics._loeT0 = t; - const α = Math.min(1, (t - updatePhysics._loeT0) / LOE_SETTLE_MS); - - // Blend current values toward LOE targets - MW = MW + (LOE_REV_PWR_MW - MW) * α; // small reverse - Q = Q + (LOE_Q_IMPORT_MVAR - Q) * α; // inductive import (positive) - } else { - delete updatePhysics._loeT0; - } - // ---------------------------------------------------------------------- - - // Apparent + amps + PF - const S = Math.sqrt(MW*MW + Q*Q); - const I_kA = (S) / (Math.sqrt(3) * (Vbus)); - const I_A = I_kA * 1000; - const PFmag = S > 1e-6 ? (Math.abs(MW) / S) : 0; - const PFsigned = (Q < 0 ? -PFmag : PFmag); - - state.MW = MW; - state.MVAR = Q; - state.AMPS = I_A; - state.PF = PFsigned; - } - - - - /* ///////////// Section 5.N slewGenKV (kV tracker) ///////////// */ - function slewGenKV(target, rate_kV_per_s){ - const now = performance.now(); - if (kvRamp.active){ - const p = clamp((now - kvRamp.t0) / kvRamp.dur, 0, 1); - state.Gen_kV_Var = kvRamp.from + (kvRamp.to - kvRamp.from) * p; - if (p >= 1) kvRamp.active = false; - return; - } - if (typeof slewGenKV._tPrev !== 'number') slewGenKV._tPrev = now; - const dt = Math.min(100, now - slewGenKV._tPrev)/1000; - slewGenKV._tPrev = now; - const step = rate_kV_per_s * dt; - const err = target - state.Gen_kV_Var; - if (Math.abs(err) <= step){ - state.Gen_kV_Var = target; - } else { - state.Gen_kV_Var += Math.sign(err) * step; - } - state.Gen_kV_Var = clamp(state.Gen_kV_Var, KV_MIN, KV_MAX); - } - - /* ///////////// Section 5.O Syncroscope & Lamps ///////////// */ - /* ///////////// Section 5.O.0 PhaseTracker (shared Δθ + snapshot) ///////////// */ - const PhaseTracker = { - delta: 0, - tPrev: null, - snap: null, // { vb, vg, fb, fg, dphiDeg, ts } - - update(fb, fg) { - const t = performance.now() * 0.001; - if (this.tPrev == null) this.tPrev = t; - let dt = t - this.tPrev; - if (dt > 0.2) dt = 0.2; - if (dt < 0) dt = 0; - this.tPrev = t; - - const df = (fg || 0) - (fb || 0); - const maxStep = Math.min(1/120, 0.5 / Math.max(1e-6, Math.abs(df))) * 0.999; - while (dt > 0) { - const step = Math.min(dt, maxStep); - this.delta += 2 * Math.PI * df * step; - dt -= step; - } - this.delta = normAngleRad(this.delta); - return this.deltaDeg(); - }, - - deltaDeg() { - return rad2deg(this.delta); // (-180,180] - }, - - snapshot(vb, vg, fb, fg) { - this.snap = { - vb: (vb != null ? vb : 13.8), - vg: Math.max(0, vg || 0), - fb: (fb != null ? fb : 60), - fg: (fg || 0), - dphiDeg: this.deltaDeg(), - ts: performance.now() - }; - return this.snap; - } - }; - - /* ///////////// Section 5.O.1 SYNC struct ///////////// */ - const SYNC = { - needleEl: null, center: null, - lampL: null, lampR: null, - busPhase: 0, genPhase: 0, tPrev: null - }; - - /* ///////////// Section 5.O.2 parseRotateCenter ///////////// */ - function parseRotateCenter(el) { - const t = (el.getAttribute("transform") || "").trim(); - const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); - if (m) return { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; - try { const bb = el.getBBox(); return { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } - catch { return { cx: 0, cy: 0 }; } - } - - /* ///////////// Section 5.O.3 initSyncUI ///////////// */ - function initSyncUI(){ - if (!SYNC.needleEl){ - SYNC.needleEl = document.getElementById("SyncScope_Rotation"); - if (SYNC.needleEl) SYNC.center = parseRotateCenter(SYNC.needleEl); - } - if (!SYNC.lampL) SYNC.lampL = document.getElementById('Glow_SyncLeft'); - if (!SYNC.lampR) SYNC.lampR = document.getElementById('Glow_SyncRight'); - } - - /* ///////////// Section 5.O.4 updateSyncScopeAndLamps ///////////// */ - function updateSyncScopeAndLamps(){ - initSyncUI(); - const snap = PhaseTracker.snap; - if (!snap) return; - - const DEFAULT_SYNC_ANGLE = 0; // needle's default clamped angle - const fg = +state.Gen_Freq_Var || 0; - const syncSwitchOff = !state.Sync_On; - const breaker52Closed = !!state['52G_Brk_Var']; // TRUE = closed - const clampNeedle = (Math.abs(fg) < 0.01) || syncSwitchOff || breaker52Closed; - - // Needle angle (clamped when Hz=0 OR Sync switch OFF OR 52G closed) - if (SYNC.needleEl && SYNC.center){ - const angle = clampNeedle - ? DEFAULT_SYNC_ANGLE - : ((snap.dphiDeg % 360) + 360) % 360; // 0..360 from shared Δθ - SYNC.needleEl.setAttribute('transform', `rotate(${angle},${SYNC.center.cx},${SYNC.center.cy})`); - } - - // Lamps brightness from true differential voltage magnitude - (function(){ - const clamp01 = x => x < 0 ? 0 : (x > 1 ? 1 : x); - - const Vb = snap.vb; - const Vg = snap.vg; - const dphiRad = deg2rad(snap.dphiDeg); - - // |Vg∠θg − Vb∠θb| - const Vr_kV = Math.sqrt(Vg*Vg + Vb*Vb - 2*Vg*Vb*Math.cos(dphiRad)); - - const Vnom = (state.Bus_Voltage_kV_nom != null ? state.Bus_Voltage_kV_nom : 13.8); - const Vr_pu = Vr_kV / (Vnom || 1); - - const LAMP_GAIN = 2; - const GAMMA = 1.1; - const SYNC_FUDGE_MIN_KV = 13.2; - const SYNC_FUDGE_MAX_KV = 14.5; - - let bright = clamp01(Math.pow(clamp01(Vr_pu * LAMP_GAIN), GAMMA)); - - // Fudge band: outside band = full ON - const inBand = (Vg > SYNC_FUDGE_MIN_KV) && (Vg < SYNC_FUDGE_MAX_KV); - if (!inBand) bright = 1.0; - - // Only show while syncing (Sync switch ON, 52G OPEN) - const syncing = !!state.Sync_On && !state['52G_Brk_Var']; - if (!syncing) bright = 0; - - if (SYNC.lampL){ SYNC.lampL.style.opacity = String(bright); SYNC.lampL.setAttribute('fill', '#FFFFFF'); } - if (SYNC.lampR){ SYNC.lampR.style.opacity = String(bright); SYNC.lampR.setAttribute('fill', '#FFFFFF'); } - })(); - } - - /* ///////////// Section 5.P Glow helpers (setGlow / setGlowWhite) ///////////// */ - function setGlow(id, on){ - const el = document.getElementById(id); - if(!el) return; - el.style.opacity = on ? '1' : '0'; - } - function setGlowWhite(id, on){ - const el = document.getElementById(id); - if(!el) return; - el.style.fill = on ? '#FFFFFF' : ''; - el.style.opacity = on ? '1' : '0'; - } - - /* ///////////// Section 5.Q updateGlows (REPLACE this section) ///////////// */ - function updateGlows(){ - const is52Open = !state['52G_Brk_Var']; - const is41Closed = !!state['41_Brk_Var']; - - // 86G permissive & flag based ONLY on knob position - const k86 = (typeof knobStates !== 'undefined') ? knobStates['Knob_86G'] : null; - const ang = (k86 && typeof k86.currentAngle === 'number') ? k86.currentAngle : 0; - const is86NormalByKnob = (ang > -1); - - // Permissives - setGlow('Glow_Perm_52G', is52Open); - setGlow('Glow_Perm_86G', is86NormalByKnob); - setGlow('Glow_Perm_Speed', !!state.Speed_Perm_Var); - setGlow('Glow_Perm_Excitation', is41Closed); - setGlowWhite('Glow_Perm_SyncCheck', !!state.SyncCheck_Perm_Var); // white - - // 52G status - setGlow('Glow_Green_52G', is52Open); - setGlow('Glow_Red_52G', !is52Open); - - // 41 status - setGlow('Glow_Green_41', !is41Closed); - setGlow('Glow_Red_41', is41Closed); - - // Ensure 86G flag tracks knob position continuously - try { setFlag86(); } catch(_){} - } - - - /* ///////////// Section 5.R updateGateGauge ///////////// */ - // Gate position (GatePos_Rotation, 0%->36°, 100%->324°) - function updateGateGauge(){ - const el = document.getElementById("GatePos_Rotation"); - if (!el) return; - const pct = clamp(state.Gate_Pos_Var, 0, 100); - const minAng = 36, maxAng = 324; - const ang = minAng + (pct / 100) * (maxAng - minAng); - const center = (function(){ - const t = (el.getAttribute("transform") || "").trim(); - const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); - if (m) return { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; - try { const bb = el.getBBox(); return { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } - catch { return { cx: 0, cy: 0 }; } - })(); - el.setAttribute("transform", `rotate(${ang},${center.cx},${center.cy})`); - } - - /* ///////////// Section 5.S Hz needle binding (IIFE) ///////////// */ - (function(){ - const PTS = [ - {hz: 0, ang: 36}, - {hz: 59, ang: 108}, - {hz: 60, ang: 180}, - {hz: 61, ang: 252}, - {hz: 62, ang: 324}, - ]; - function mapHzToAngle(hz) { - if (!isFinite(hz)) return 36; - hz = clamp(hz, 0, 62); - for (let i = 0; i < PTS.length - 1; i++) { - const a = PTS[i], b = PTS[i+1]; - if (hz >= a.hz && hz <= b.hz) { - const t = (hz - a.hz) / (b.hz - a.hz || 1); - return a.ang + t * (b.ang - a.ang); - } - } - return 324; - } - let hzEl = null, center = null; - function initHz() { - hzEl = document.getElementById("Hz_Rotation"); - if (!hzEl) return false; - const t = (hzEl.getAttribute("transform") || "").trim(); - const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); - if (m) center = { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; - else { - try { const bb = hzEl.getBBox(); center = { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } - catch { center = { cx: 0, cy: 0 }; } - } - return true; - } - function updateRPMText() { - const el = document.getElementById("Value_RPM"); - if (!el) return; - const v = Math.round(state.Master_Started ? (state.Gen_RPM_Var || 0) : 0); - if (el.textContent !== String(v)) el.textContent = String(v); - } - function updateHz() { - if (!hzEl && !initHz()) return; - const angle = mapHzToAngle(state.Master_Started ? (state.Gen_Freq_Var || 0) : 0); - hzEl.setAttribute("transform", `rotate(${angle},${center.cx},${center.cy})`); - } - function tick(){ updateRPMText(); updateHz(); } - document.addEventListener("DOMContentLoaded", () => { initHz(); tick(); }); - clearInterval(window.__v6k_iv); - window.__v6k_iv = setInterval(tick, 100); - })(); - - /* ///////////// Section 5.T updateKVgauge ///////////// */ - // Gen Volts gauge (GenVolts_Rotation: 36°=0 kV, 180°=13 kV, 324°=15 kV) - function updateKVgauge(){ - const el = document.getElementById("GenVolts_Rotation"); - if (!el) return; - const v = clamp(state.Gen_kV_Var, 0, 15); // gauge tops at 15 - let ang; - if (v <= 13){ - // map 0..13 kV to 36..180° - const t = v / 13; - ang = 36 + t * (180 - 36); - } else { - // map 13..15 to 180..324° - const t = (v - 13) / 2; - ang = 180 + t * (324 - 180); - } - const center = (function(){ - const t = (el.getAttribute("transform") || "").trim(); - const m = t.match(/rotate\(\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*,\s*([\-0-9.]+)\s*\)/i); - if (m) return { cx: parseFloat(m[2]), cy: parseFloat(m[3]) }; - try { const bb = el.getBBox(); return { cx: bb.x + bb.width/2, cy: bb.y + bb.height/2 }; } - catch { return { cx: 0, cy: 0 }; } - })(); - el.setAttribute("transform", `rotate(${ang},${center.cx},${center.cy})`); - } - - /* ///////////// Section 5.U fmtNoLeadingZeros ///////////// */ - function fmtNoLeadingZeros(n, decimals){ - if (!isFinite(n)) return '0'; - const s = (decimals != null) ? n.toFixed(decimals) : String(Math.round(n)); - return s; - } - - /* ///////////// Section 5.V updateDigitals ///////////// */ - function updateDigitals(){ - const elMW = document.getElementById("Value_MW"); - const elA = document.getElementById("Value_AMPS"); - const elMVAR = document.getElementById("Value_MVAR"); - const elPF = document.getElementById("Value_PowerFactor"); - - const MW = +state.MW || 0; - const MVAR = +state.MVAR || 0; - const AMPS = +state.AMPS || 0; - - // PF display — show "unity at tiny load" only when AVR is ON - const S_MVA = Math.sqrt(MW*MW + MVAR*MVAR); - const PF_SMALL_THRESH_MVA = 0.3; - let pfDisp; - if (state.AVR_On && state['52G_Brk_Var'] && S_MVA < PF_SMALL_THRESH_MVA){ - pfDisp = (MVAR < 0 ? -1 : 1); - } else if (S_MVA < 1e-6){ - pfDisp = 1; - } else { - pfDisp = clamp(+state.PF || 0, -1, 1); - } - - if (elMW){ - const s = fmtNoLeadingZeros(MW, 1); - if (elMW.textContent !== s) elMW.textContent = s; - } - if (elA){ - const s = fmtNoLeadingZeros(AMPS, 0); - if (elA.textContent !== s) elA.textContent = s; - } - if (elMVAR){ - const s = fmtNoLeadingZeros(MVAR, 1); - if (elMVAR.textContent !== s) elMVAR.textContent = s; - } - if (elPF){ - const s = fmtNoLeadingZeros(pfDisp, 2); - if (elPF.textContent !== s) elPF.textContent = s; - } - } - - - - /* ///////////// Section 5.W Main tick loop (requestAnimationFrame) ///////////// */ - function tick(){ - watchAngles(); - updateGateSet(); - updateVoltageSet(); - updatePhysics(); - - // Instant zero kV if 52G CLOSED and 41 OPEN - if (state['52G_Brk_Var'] && !state['41_Brk_Var']) { - state.Gen_kV_Var = 0; - } - - // ---- Shared phase + snapshot (single source of truth) ---- - const fb = +state.Bus_Freq_Hz || 60; - const fg = +state.Gen_Freq_Var || 0; - const vb = +state.Bus_Voltage_kV || 13.8; - const vg = +state.Gen_kV_Var || 0; - PhaseTracker.update(fb, fg); - PhaseTracker.snapshot(vb, vg, fb, fg); - // ---------------------------------------------------------- - - updateSyncCheck(); - updateGlows(); - updateGateGauge(); - updateKVgauge(); - updateSyncScopeAndLamps(); - if (window.Osc && typeof window.Osc.update === 'function') window.Osc.update(); // Oscilloscope - updateDigitals(); - requestAnimationFrame(tick); - } - requestAnimationFrame(tick); - - - - /* ///////////// Section 5.Y — Oscilloscope (Bus & Gen waveforms) ///////////// */ - (function(){ - const Osc = { - inited:false, - els:{ bg:null, bus:null, gen:null }, - rect:null, - busDrawn:false, - - $: id => document.getElementById(id), - - ensureInit(){ - if (this.inited) return true; - this.els.bg = this.$('Osc_Background'); - this.els.bus = this.$('Osc_BusWave'); - this.els.gen = this.$('Osc_GenWave'); - if (!this.els.bg || !this.els.bus || !this.els.gen) return false; - - const x = parseFloat(this.els.bg.getAttribute('x')) || 0; - const y = parseFloat(this.els.bg.getAttribute('y')) || 0; - const w = parseFloat(this.els.bg.getAttribute('width')) || (this.els.bg.getBBox?.().width || 0); - const h = parseFloat(this.els.bg.getAttribute('height')) || (this.els.bg.getBBox?.().height || 0); - this.rect = { x, y, w, h, cx:x+w/2, cy:y+h/2 }; - this.inited = true; - return true; - }, - - sinePath(x, w, cy, cycles, amp, phaseRad, samples){ - const k = 2*Math.PI*cycles; - let d = `M ${x} ${ (cy - amp*Math.sin(phaseRad)).toFixed(1) }`; - for(let i=1;i<=samples;i++){ - const t = i/samples; - const xx = x + t*w; - const yy = cy - amp*Math.sin(k*t + phaseRad); - d += ` L ${xx.toFixed(1)} ${yy.toFixed(1)}`; - } - return d; - }, - - drawBusOnce(){ - if (!this.ensureInit()) return; - if (this.busDrawn) return; - const r = this.rect; - const A = 0.90*(r.h/2); - const cycles = 4; - const d = this.sinePath(r.x, r.w, r.cy, cycles, A, 0, 480); - this.els.bus.setAttribute('d', d); - this.busDrawn = true; - }, - - updateGen(){ - if (!this.ensureInit()) return; - const r = this.rect; - const s = (window.SimState || window.state || {}); - const snap = (typeof PhaseTracker !== 'undefined' && PhaseTracker.snap) ? PhaseTracker.snap : null; - - const fg = +s.Gen_Freq_Var || 0; - const vGen = Math.max(0, +s.Gen_kV_Var || 0); - const vNom = (+s.Bus_Voltage_kV_nom) || (+s.Bus_Voltage_kV) || 13.8; - - const Amax = 0.90*(r.h/2); - const A = Amax * (vGen / (vNom || 13.8)); - - const busHz = (+s.Bus_Freq_Hz) || 60; - const T_win = 4 / busHz; // ~4 bus cycles across width - const cyclesG = fg * T_win; - - // Clamp phase to 0° when 52G is CLOSED (snap in-phase with bus) - const synced = !!s['52G_Brk_Var']; - const phiGen = synced ? 0 : (snap ? deg2rad(snap.dphiDeg) : 0); - - const d = this.sinePath(r.x, r.w, r.cy, cyclesG, A, phiGen, 480); - this.els.gen.setAttribute('d', d); - }, - - update(){ - this.drawBusOnce(); - this.updateGen(); - } - }; - - try { window.Osc = Osc; } catch(_){} - })(); - - - /* ///////////// Section 5.Z Protections ///////////// */ - (function ProtectionsAddon(){ - const S = window.SimState || window.state || (window.state = {}); - Object.assign(S, { - Alarm_32:false, Alarm_55:false, Alarm_27:false, Alarm_59:false, Alarm_81:false, - Trip_32:false, Trip_40:false, Trip_27_59:false, Trip_81:false - }); - - const PROT = (window.__PROT__ = window.__PROT__ || { - tprev: performance.now(), lastNow: 0, dtCache: 0, - prev52: !!S['52G_Brk_Var'], - inhibit32UntilMs: 0, - a32:0, a55:0, a55sev:0, a27:0, a59:0, a81UF:0, a81OF:0, - t32:0, t40A:0, t40B:0, t27:0, t59:0, t27Backup:0, t59Backup:0, t81UF:0, t81OF:0, - }); - - function getDt(){ - const now = performance.now(); - if (now - PROT.lastNow < 5) return PROT.dtCache; - const dt = Math.max(0, (now - PROT.tprev)/1000); - PROT.tprev = now; PROT.lastNow = now; PROT.dtCache = dt; - return dt; - } - - function logFlag(name, on){ - if (S[name] !== on){ - S[name] = on; - - if (name.startsWith('Alarm_')){ - const code = name.split('_')[1]; - let msg; - switch(code){ - case '32': msg = on ? 'Reverse Power Alarm' : 'Reverse Power Normal'; break; - case '55': msg = on ? 'Power Factor Alarm' : 'Power Factor Normal'; break; - case '27': msg = on ? 'Undervoltage Alarm' : 'Voltage Normal'; break; - case '59': msg = on ? 'Overvoltage Alarm' : 'Voltage Normal'; break; - case '81': msg = on ? 'Frequency Alarm' : 'Frequency Normal'; break; - default: msg = `ALARM ${code}: ${on ? 'ACTIVE' : 'FALSE'}`; - } - try { logDebug(msg); } catch(_) {} - return; - } - - if (name === 'Trip_32' && on){ - try { logDebug('Trip: Reverse Power'); } catch(_) {} - return; - } - if (name === 'Trip_40' && on){ - try { logDebug('Trip: Loss of Field'); } catch(_) {} - return; - } - // Trip_27_59 and Trip_81 messages handled directly in evaluateTrips - } - } - - function setGlow(id, on){ - const el = document.getElementById(id); - if (!el) return; - el.style.opacity = on ? 1 : 0; - try{ el.setAttribute('opacity', on ? '1' : '0'); }catch(_){} - } - - // ---------- Alarms (self-reset) ---------- - window.evaluateAlarms = function(){ - const now = performance.now(); - const dt = getDt(); - - // Master Stop mask: block alarms during normal shutdown - if (S.MasterStopMask === true) { - logFlag('Alarm_32', false); - logFlag('Alarm_55', false); - logFlag('Alarm_27', false); - logFlag('Alarm_59', false); - logFlag('Alarm_81', false); - PROT.a32 = PROT.a55 = PROT.a27 = PROT.a59 = PROT.a81UF = PROT.a81OF = 0; - return; - } - - if (!PROT.prev52 && S['52G_Brk_Var']) PROT.inhibit32UntilMs = now + 15000; - PROT.prev52 = !!S['52G_Brk_Var']; - - if (!S.GeneratorOnline){ - logFlag('Alarm_32', false); - logFlag('Alarm_55', false); - logFlag('Alarm_27', false); - logFlag('Alarm_59', false); - logFlag('Alarm_81', false); - PROT.a32 = PROT.a55 = PROT.a27 = PROT.a59 = PROT.a81UF = PROT.a81OF = 0; - return; - } - - const RatedMW = Math.max(1e-6, +((window.RATED && RATED.MW) || 23.5)); - const MW = +S.MW || 0; - const Q = +S.MVAR || 0; - const Vpu = Math.max(0, (+S.Gen_kV_Var || 0) / ((window.RATED && RATED.KV_LL) || 13.8)); - const Hz = +S.Gen_Freq_Var || 0; - - // 32 — Reverse Power (self-reset alarm) - const ppu = MW / RatedMW; - const a32 = (ppu < -0.01); - PROT.a32 = a32 ? PROT.a32 + dt : 0; - logFlag('Alarm_32', PROT.a32 >= 0.2); - - // 55 — LOE / Field Issues (proxy alarm) - // Alarm when power factor magnitude drops below 0.9 (leading or lagging) - const Sva = Math.hypot(MW, Q); - const pf = Sva > 0 ? MW / Sva : 1; - const a55 = (Math.abs(pf) < 0.9); - PROT.a55 = a55 ? PROT.a55 + dt : 0; - logFlag('Alarm_55', PROT.a55 >= 0.2); - - // 27 — Undervoltage (self-reset, immediate) - const a27 = (Vpu < 0.96); - PROT.a27 = a27 ? PROT.a27 + dt : 0; - logFlag('Alarm_27', a27); // use a time threshold here if you want a delay - - // 59 — Overvoltage (self-reset, immediate) - const a59 = (Vpu > 1.04); - PROT.a59 = a59 ? PROT.a59 + dt : 0; - logFlag('Alarm_59', a59); // use a time threshold here if you want a delay - - // 81 — Frequency (5 s either side) - const a81UF = (Hz < 59.8); - const a81OF = (Hz > 60.2); - PROT.a81UF = a81UF ? PROT.a81UF + dt : 0; - PROT.a81OF = a81OF ? PROT.a81OF + dt : 0; - logFlag('Alarm_81', (PROT.a81UF >= 5.0) || (PROT.a81OF >= 5.0)); - }; - - // ---------- Trips (LATCHED) ---------- - window.evaluateTrips = function(){ - const dt = getDt(); - - // Master Stop mask: block trip evaluation during normal shutdown - if (S.MasterStopMask === true) { - return; - } - - if (!S.GeneratorOnline){ - // Latching: do NOT auto-clear trips when offline - return; - } - - const RatedMW = Math.max(1e-6, +((window.RATED && RATED.MW) || 23.5)); - const RatedMVA = Math.max(1e-6, +((window.RATED && RATED.MVA) || RatedMW)); - const MW = +S.MW || 0; - const Q = +S.MVAR || 0; - const Vpu = Math.max(0, (+S.Gen_kV_Var || 0) / ((window.RATED && RATED.KV_LL) || 13.8)); - const Hz = +S.Gen_Freq_Var || 0; - - // 32 — Reverse Power (0.5 s, inhibit for 15s after close) - const now = performance.now(); - const inhibit32 = (now < PROT.inhibit32UntilMs); - PROT.t32 = (MW < -0.02 * RatedMW && !inhibit32) ? PROT.t32 + dt : 0; - if (!S.Trip_32 && PROT.t32 >= 0.5) { - logFlag('Trip_32', true); - } - - // 40 — Field Breaker trip (proxy) - const t40 = (!S['41_Brk_Var'] && S['52G_Brk_Var']); - PROT.t40A = t40 ? PROT.t40A + dt : 0; - PROT.t40B = t40 ? PROT.t40B + dt : 0; - if (!S.Trip_40 && (PROT.t40A >= 0.05 || PROT.t40B >= 0.05)) { - logFlag('Trip_40', true); - } - - // 27/59 — Voltage trips (0.5 s either side) - const v27 = (Vpu < 0.90); - const v59 = (Vpu > 1.10); - PROT.t27 = v27 ? PROT.t27 + dt : 0; - PROT.t59 = v59 ? PROT.t59 + dt : 0; - - const trip27 = (PROT.t27 >= 0.5); - const trip59 = (PROT.t59 >= 0.5); - if ((!S.Trip_27_59) && (trip27 || trip59)) { - logFlag('Trip_27_59', true); - try { - if (trip27) logDebug('Trip: Undervoltage'); - if (trip59) logDebug('Trip: Overvoltage'); - } catch(_) {} - } - - // 81 — Frequency - PROT.t81UF = (Hz < 59.0) ? PROT.t81UF + dt : 0; - PROT.t81OF = (Hz > 61.5) ? PROT.t81OF + dt : 0; - if (!S.Trip_81 && ((PROT.t81UF >= 0.5) || (PROT.t81OF >= 0.5))) { - logFlag('Trip_81', true); - try { - if (PROT.t81UF >= 0.5) logDebug('Trip: Underfrequency'); - if (PROT.t81OF >= 0.5) { logDebug('Overspeed Detected'); logDebug('Trip: Overfrequency'); } - } catch(_) {} - } - }; - - window.updateProtectionGlows = function(){ - setGlow('Glow_Alarm_32', S.Alarm_32 === true); - setGlow('Glow_Alarm_55', S.Alarm_55 === true); - setGlow('Glow_Alarm_27', (S.Alarm_27 === true) || (S.Alarm_59 === true)); - setGlow('Glow_Alarm_81', S.Alarm_81 === true); - setGlow('Glow_Trip_32', S.Trip_32 === true); - setGlow('Glow_Trip_40', S.Trip_40 === true); - setGlow('Glow_Trip_27', S.Trip_27_59 === true); - setGlow('Glow_Trip_81', S.Trip_81 === true); - setGlow('Glow_Alarm_86G', S['86G_Trip_Var'] === true); - }; - - // Fallback looper - if (!window.__PROT_LOOP__){ - window.__PROT_LOOP__ = true; - (function loop(){ - try{ - if (typeof window.evaluateAlarms === 'function') window.evaluateAlarms(); - if (typeof window.evaluateTrips === 'function') window.evaluateTrips(); - if (typeof window.updateProtectionGlows === 'function') window.updateProtectionGlows(); - }catch(_){} - requestAnimationFrame(loop); - })(); - } - })(); - - - - /* ///////////// Section 5.Z1: Consolidated 86G Trip & Knob Management ///////////// */ - (function Auto86GTripAndKnobManager() { - const KNOB_ID = 'Knob_86G'; - const TRIP_ANGLE = -45; - const RESET_ANGLE = 0; - - const switchConfig = switches.find(s => s.knobId === KNOB_ID); - const originalType = switchConfig ? switchConfig.type : 'latching'; - // Timer to ensure gates remain <=20% for a short period before - // latching 86G. This avoids premature trip commands while the - // governor is still driving the gates closed. - let belowThreshSince = null; - - function rotateKnob(angle) { - const state = knobStates[KNOB_ID]; - const el = document.getElementById(KNOB_ID); - if (!state || !el) return; - const cx = state.centerX, cy = state.centerY; - state.currentAngle = angle; - if (prevAngles) prevAngles[KNOB_ID] = angle; - el.setAttribute('transform', `rotate(${angle} ${cx} ${cy})`); - } - - function loop() { - const s = window.SimState || window.state; - const k = knobStates[KNOB_ID]; - if (!s || !switchConfig || !k) { requestAnimationFrame(loop); return; } - - const isTripped = s['86G_Trip_Var']; - const anyProt = !!(s.Trip_32 || s.Trip_40 || s.Trip_27_59 || s.Trip_81); - - // Driveback and Auto-trip logic - if (anyProt && !isTripped) { - // Initiate driveback: set gate setpoint to 0 immediately. - // This will cause the actual gate position (Gate_Pos_Var) to ramp down. - Gate_Setpoint = 0; - gateRamp.active = false; // Ensure any conflicting start ramps are cancelled. - - // Once gates fall to/below 20%, start a hold timer. When - // they remain below threshold for 500 ms, trip 86G. - if (s.Gate_Pos_Var <= 20) { - if (belowThreshSince === null) { - belowThreshSince = performance.now(); - } else if (performance.now() - belowThreshSince >= 500) { - handleAction('86G_TRIP'); - rotateKnob(TRIP_ANGLE); // Visually snap knob to trip position. - belowThreshSince = null; - } - } else { - // Gates above threshold again before timeout; reset timer - belowThreshSince = null; - } - } else { - // reset when no protection trip or already tripped - belowThreshSince = null; - } - - // Behavior while tripped (unchanged from original) - if (isTripped) { - switchConfig.type = 'momentary'; - k.momentaryReturnAngle = TRIP_ANGLE; - switchConfig.maxAngle = -5; - - if (!k.isDragging && k.currentAngle !== TRIP_ANGLE) { - rotateKnob(TRIP_ANGLE); - } - } else { - // Reset restores latching behavior (unchanged from original) - switchConfig.type = originalType; - k.momentaryReturnAngle = null; - switchConfig.maxAngle = RESET_ANGLE; - } - - requestAnimationFrame(loop); - } - loop(); - })(); - - /* ///////////// Section 5.Z2 — Reset Button Debug Hook ///////////// */ - (function ResetButtonDebugHook(){ - function wire(){ - const btn = document.getElementById('Button_RESET'); // <-- corrected ID - if(!btn || btn.dataset._dbgHooked) return !!btn; - btn.dataset._dbgHooked = '1'; - - - // Use click for safety - btn.addEventListener('click', () => { - - }, {passive:true}); - - return true; - } - - if(!wire()){ - let tries = 0; - const iv = setInterval(() => { - if (wire() || ++tries > 50) clearInterval(iv); - }, 100); - } - })(); - - /* ///////////// Section 5.Z3 — Reset Handler///////////// */ - (function ResetHandler(){ - function doReset(){ - // Clear all auto trip latches (both internal vars and manager flags) - state['32_Trip_Var'] = false; - state['40_Trip_Var'] = false; - state['27_59_Trip_Var'] = false; - state['81_Trip_Var'] = false; - state['Trip_32'] = false; - state['Trip_40'] = false; - state['Trip_27_59'] = false; - state['Trip_81'] = false; - - // Clear lockout latch (86G) - state['86G_Trip_Var'] = false; - try { setFlag86(false); } catch(_){} - - // Clear online latch so logic can restart clean - state['GeneratorOnline'] = false; - - // Refresh lamps - try { updateProtectionGlows(); } catch(_){} - - // Debug - try { - - } catch(_){ - console.log('RESET: Trips (32/40/27-59/81) and 86G cleared; GeneratorOnline reset'); - } - } - - const btn = document.getElementById('Button_RESET'); - if(btn && !btn.dataset._resetHandler){ - btn.dataset._resetHandler = '1'; - btn.addEventListener('mousedown', doReset, {passive:true}); - } - })(); - - /* ///////////// Section 5.Z4 — Master Stop Protections Inhibit (non-invasive) ///////////// */ - (function MasterStopProtectionsInhibit(){ - const S = window.SimState || window.state || (window.state = {}); - if (typeof S.MasterStopMask !== 'boolean') S.MasterStopMask = false; - - const origHandle = (typeof window.handleAction === 'function') ? window.handleAction : null; - window.handleAction = function(tag){ - try{ - if (tag === 'MASTER_STOP') { - if (!S.MasterStopMask){ - S.MasterStopMask = true; - } - } else if (tag === 'MASTER_START') { - if (S.MasterStopMask){ - S.MasterStopMask = false; - } - } - }catch(_){} - return origHandle ? origHandle.apply(this, arguments) : undefined; - }; - - // Auto-clear when fully stopped - if (!window.__MS_MASK_LOOP__){ - window.__MS_MASK_LOOP__ = true; - (function loop(){ - try{ - if (S.MasterStopMask){ - const stopped = (!S['52G_Brk_Var']) && - (!S.Master_Started) && - ((+S.Gate_Pos_Var || 0) <= 0.5) && - ((+S.Gen_Freq_Var || 0) <= 0.2); - if (stopped){ - S.MasterStopMask = false; - } - } - }catch(_){} - requestAnimationFrame(loop); - })(); - } - })(); - - - })(); - - -/* ///////////// Section 6 — RPM Text Binding (IIFE) ///////////// */ -(function () { - function getState() { return window.state || window.SimState || null; } - function calcRPM() { - const s = getState(); - if (!s) return null; - if (typeof s.Gen_RPM_Var === "number") return s.Gen_RPM_Var; - if (typeof s.Gen_Freq_Var === "number") return s.Gen_Freq_Var * 1.667; - return null; - } - function updateRPMText() { - const el = document.getElementById("Value_RPM"); - if (!el) return; - const rpm = calcRPM(); - if (rpm == null || isNaN(rpm)) return; - const v = Math.round(rpm); - if (el.textContent !== String(v)) el.textContent = String(v); - } - clearInterval(window.__rpm_bind_iv); - window.__rpm_bind_iv = setInterval(updateRPMText, 200); - document.addEventListener("DOMContentLoaded", updateRPMText); - -})(); - diff --git a/Sim_Manual.pdf b/Sim_Manual.pdf deleted file mode 100644 index c4483c2ec0f4590201e3db15d13bdef624069635..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83948 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

;(_I>?7x$M=2p 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 UCZ>?=Ow4R-kR&7`a-xv`2SZ!V<^TWy diff --git a/googled9d1eff3cee3f7bb.html b/googled9d1eff3cee3f7bb.html deleted file mode 100644 index b268ae4..0000000 --- a/googled9d1eff3cee3f7bb.html +++ /dev/null @@ -1 +0,0 @@ -google-site-verification: googled9d1eff3cee3f7bb.html \ No newline at end of file diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 172ebdd..0000000 --- a/netlify.toml +++ /dev/null @@ -1,24 +0,0 @@ -# Sitemaps & robots: correct type + avoid stale cache -[[headers]] -for = "/sitemap.xml" - [headers.values] - Content-Type = "application/xml; charset=utf-8" - Cache-Control = "no-cache" - -[[headers]] -for = "/sitemap_index.xml" - [headers.values] - Content-Type = "application/xml; charset=utf-8" - Cache-Control = "no-cache" - -[[headers]] -for = "/robots.txt" - [headers.values] - Content-Type = "text/plain; charset=utf-8" - Cache-Control = "no-cache" - -# Ensure indexing is allowed everywhere -[[headers]] -for = "/*" - [headers.values] - X-Robots-Tag = "all" diff --git a/readme.md b/readme.md deleted file mode 100644 index 20f30cf..0000000 --- a/readme.md +++ /dev/null @@ -1,16 +0,0 @@ -# Generator Synchroscope (Syncroscope) Simulator - -**Live demo:** https://jrasmussen-gensync.netlify.app/ -**Manual (PDF):** [Operator & Technical Manual](Sim_Manual.pdf) -**Source code:** [https://github.com/jonrack77/GeneratorSync](https://github.com/jonrack77/GeneratorSync) - -A free, web-based hydroelectric **generator synchronization** trainer. Practice using a **synchroscope** with **AVR/excitation** and **governor** controls while managing **power factor**, **phase angle**, and **frequency**. - -## Features -- Synchroscope UI with breaker close logic -- AVR / excitation and governor adjustments -- PF, phase angle, and frequency training -- Manual available as PDF for reference - -## Keywords -generator, synchroscope, syncroscope, generator synchronization, grid sync, power systems training, simulator, AVR, excitation, governor, power factor, phase angle, frequency, hydroelectric, hydropower diff --git a/robots.txt b/robots.txt deleted file mode 100644 index 927288f..0000000 --- a/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://jrasmussen-gensync.netlify.app/sitemap.xml - diff --git a/sitemap.txt b/sitemap.txt deleted file mode 100644 index 291c7fe..0000000 --- a/sitemap.txt +++ /dev/null @@ -1,8 +0,0 @@ - - - - https://jrasmussen-gensync.netlify.app/ - weekly - 1.0 - - diff --git a/sitemap.xml b/sitemap.xml deleted file mode 100644 index c69cd96..0000000 --- a/sitemap.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - https://jrasmussen-gensync.netlify.app/ - 2025-08-22 - weekly - 1.0 - - diff --git a/sitemap_index.xml b/sitemap_index.xml deleted file mode 100644 index 5fed3fb..0000000 --- a/sitemap_index.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - https://jrasmussen-gensync.netlify.app/sitemap.xml - 2025-08-22 - - diff --git a/yandex_4c440f91da05aaad.html b/yandex_4c440f91da05aaad.html deleted file mode 100644 index 5093007..0000000 --- a/yandex_4c440f91da05aaad.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - Verification: 4c440f91da05aaad - \ No newline at end of file