diff --git a/.github/workflows/candidate-package.yml b/.github/workflows/candidate-package.yml index b82a565f..60e02c61 100644 --- a/.github/workflows/candidate-package.yml +++ b/.github/workflows/candidate-package.yml @@ -36,7 +36,7 @@ env: jobs: candidate: name: Build verified Windows x64 candidate - if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'stabilization/') || startsWith(github.head_ref, 'architecture/') || startsWith(github.head_ref, 'legal/') + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'stabilization/') || startsWith(github.head_ref, 'architecture/') || startsWith(github.head_ref, 'legal/') || startsWith(github.head_ref, 'fix/') runs-on: windows-latest timeout-minutes: 35 diff --git a/docs/SV_ENGINEERING_SCALE_LAB.md b/docs/SV_ENGINEERING_SCALE_LAB.md new file mode 100644 index 00000000..9c3717ad --- /dev/null +++ b/docs/SV_ENGINEERING_SCALE_LAB.md @@ -0,0 +1,42 @@ +# SV Engineering Units — independent capture lab + +Process Bus Insight is receive-only. It cannot prove physical A/V scaling from +IEC 61850 SV packet identity, raw payload width, APPID, svID or a waveform plot. + +## Simple workflow (no SCL required) + +1. Run the publisher, select the receiving Ethernet/Npcap adapter in Process + Bus Insight, press **Run**, then select the *canonical* stream under SV Stream + Explorer. The separate broadcast diagnostic mirror remains independent. +2. Check waveform and phase relationships immediately; this works in **Raw + counts (uncalibrated)** mode. RMS and waveform axes indicate **counts**. + P/Q/S/PF are unavailable rather than fictional MW/Mvar. +3. In **Metering > SV units**, opt in to **4I+4V nominal (1 mA / 10 mV per + count)** *only* when that is the known mapping of the selected lab publisher. + For example, raw 1000 corresponds to 1.000 A; raw 5774 corresponds to + 57.74 V. This is not independent calibration or a universal SV rule. +4. Check A/V RMS, 50 Hz, coherent phasors, waveform, P/Q/S and power factor. + Try changing a single phase while the publisher runs; compare the + timestamp-aligned evidence rather than screenshots taken at different times. +5. Use **Diagnostics > Copy Evidence**. Text includes per-stream SV identity, + sample counters, decode and timing evidence, the selected engineering scale, + the operator authority warning, RMS A/V/count values and P/Q/S/PF. +6. Select another stream: it defaults to **Raw counts** until explicitly + configured. Return to the first stream to restore its own scaling choice. + A fresh capture reset clears the analyzer's runtime state; select the + nominal profile again when needed. + +## Acceptance boundaries + +- Known 4I+4V: `I=0.001 A/count`, `U=0.01 V/count`; voltage/current axes + have separate autorange and do not accept fixed A/V limits while uncalibrated. +- One coherent selected-stream snapshot owns waveform, RMS, phasor and power. +- Pure power regression: three phases at 57.74 V, 1 A and 30 degree angle + offset produce roughly 173.22 VA, 150 W and 86.61 var. Any raw-count, + missing-phase or nonfinite input rejects physical power. +- Packet quality, sample continuity, VLAN visibility, Npcap capture drops, + host arrival jitter and hardware PTP lock remain separate facts. +- Use locally held operator PCAP and physical ESP32-P4 to validate Windows + candidate; do not commit private recordings or identifying site data. +- A successful waveform or power calculation does not prove calibration, + hardware timestamp quality or that a real IED accepted SV. diff --git a/src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs b/src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs index ddc1dc2e..629d561b 100644 --- a/src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs +++ b/src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs @@ -214,9 +214,9 @@ protected override void OnRender(DrawingContext dc) var visibleLanes = new List(2); if (showVoltage) - visibleLanes.Add(new LaneDefinition("Voltage", "V", snapshot.VoltageSeries)); + visibleLanes.Add(new LaneDefinition("Voltage", snapshot.VoltageSeries.FirstOrDefault()?.Unit ?? "counts", snapshot.VoltageSeries)); if (showCurrent) - visibleLanes.Add(new LaneDefinition("Current", "A", snapshot.CurrentSeries)); + visibleLanes.Add(new LaneDefinition("Current", snapshot.CurrentSeries.FirstOrDefault()?.Unit ?? "counts", snapshot.CurrentSeries)); if (visibleLanes.Count == 0) { @@ -521,13 +521,16 @@ private static double[] BuildDisplaySamples(IReadOnlyList source, int vi private double ResolveLaneAmplitudeMax(LaneDefinition lane, IReadOnlyList series) { - var isVoltage = string.Equals(lane.Unit, "V", StringComparison.OrdinalIgnoreCase); + // A raw-count voltage lane is still the voltage lane. Its history + // must not share the current lane auto-range, nor adopt fixed A/V scales. + var isVoltage = string.Equals(lane.Title, "Voltage", StringComparison.OrdinalIgnoreCase); + var hasPhysicalUnits = string.Equals(lane.Unit, isVoltage ? "V" : "A", StringComparison.OrdinalIgnoreCase); var scaleText = isVoltage ? VoltageScale : CurrentScale; - if (isVoltage && TryParseVoltageScalePeak(scaleText, out var fixedVoltagePeak)) + if (hasPhysicalUnits && isVoltage && TryParseVoltageScalePeak(scaleText, out var fixedVoltagePeak)) return Math.Max(fixedVoltagePeak, 1.0); - if (!isVoltage && TryParseCurrentScalePeak(scaleText, out var fixedCurrentPeak)) + if (hasPhysicalUnits && !isVoltage && TryParseCurrentScalePeak(scaleText, out var fixedCurrentPeak)) return Math.Max(fixedCurrentPeak, 1.0); var observedPeak = ResolveRobustObservedPeak(series); diff --git a/src/ProcessBus.App.Wpf/MainWindow.xaml b/src/ProcessBus.App.Wpf/MainWindow.xaml index 3f1b4661..c19ad425 100644 --- a/src/ProcessBus.App.Wpf/MainWindow.xaml +++ b/src/ProcessBus.App.Wpf/MainWindow.xaml @@ -2413,6 +2413,23 @@ + + + +