Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/candidate-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions docs/SV_ENGINEERING_SCALE_LAB.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 8 additions & 5 deletions src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@
return new Size(Math.Max(480, width), Math.Max(320, height));
}

protected override void OnRender(DrawingContext dc)

Check warning on line 198 in src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

In member void WaveformPanelControl.OnRender(DrawingContext dc), change parameter name dc to drawingContext in order to match the identifier as it has been declared in void UIElement.OnRender(DrawingContext drawingContext) (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1725)
{
base.OnRender(dc);

Expand All @@ -214,9 +214,9 @@

var visibleLanes = new List<LaneDefinition>(2);
if (showVoltage)
visibleLanes.Add(new LaneDefinition("Voltage", "V", snapshot.VoltageSeries));
visibleLanes.Add(new LaneDefinition("Voltage", snapshot.VoltageSeries.FirstOrDefault()?.Unit ?? "counts", snapshot.VoltageSeries));

Check warning on line 217 in src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

Do not use Enumerable methods on indexable collections. Instead use the collection directly. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1826)
if (showCurrent)
visibleLanes.Add(new LaneDefinition("Current", "A", snapshot.CurrentSeries));
visibleLanes.Add(new LaneDefinition("Current", snapshot.CurrentSeries.FirstOrDefault()?.Unit ?? "counts", snapshot.CurrentSeries));

Check warning on line 219 in src/ProcessBus.App.Wpf/Controls/WaveformPanelControl.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

Do not use Enumerable methods on indexable collections. Instead use the collection directly. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1826)
Comment thread
masarray marked this conversation as resolved.

if (visibleLanes.Count == 0)
{
Expand Down Expand Up @@ -521,13 +521,16 @@

private double ResolveLaneAmplitudeMax(LaneDefinition lane, IReadOnlyList<WaveformSeriesModel> 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);
Expand Down
17 changes: 17 additions & 0 deletions src/ProcessBus.App.Wpf/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2413,6 +2413,23 @@
</Border>
</DockPanel>

<WrapPanel Margin="0,0,0,6">
<TextBlock Text="SV units" Foreground="#B3C9DD" Margin="0,6,8,0" FontSize="11"/>
<ComboBox Width="260"
Style="{StaticResource CompactComboBoxStyle}"
ItemsSource="{Binding EngineeringScaleOptions}"
SelectedItem="{Binding SelectedEngineeringScale, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ToolTip="Choose the physical scale for this selected SV stream. No SCL required; raw counts is safest until the profile is verified."/>
Comment thread
masarray marked this conversation as resolved.
<Button Content="Copy diagnostics" Command="{Binding CopyEvidenceCommand}"
Width="138" Height="27" Margin="0,3,6,3"
ToolTip="Copy selected stream, packet counters, scaling, RMS and power evidence to clipboard."/>
<TextBlock Text="{Binding EvidenceCopyStatusText}"
Foreground="#9BB8D0" Margin="0,8,0,0" FontSize="10"/>
</WrapPanel>
<TextBlock Text="{Binding EngineeringScaleExplanation}"
Foreground="#F0B533" TextWrapping="Wrap" FontSize="10.5"
Margin="0,0,0,9"/>

<Border Margin="0,0,0,8"
Visibility="Collapsed"
Padding="8,6"
Expand Down
94 changes: 65 additions & 29 deletions src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
private WaveformSnapshot _displayedWaveform = new();
private string? _lastRequestedRawStreamId;
private readonly Dictionary<string, StreamWorkspaceCache> _streamWorkspaces = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, SvEngineeringScale> _explicitSvScales = new(StringComparer.OrdinalIgnoreCase);
private const string RawScaleText = "Raw counts (uncalibrated)";
private const string NominalScaleText = "4I+4V nominal (1 mA / 10 mV per count)";
private string _waveformShowMode = "Both";
private string _waveformLayoutMode = "Overlay";
private string _waveformTimebase = "2 cycles";
Expand Down Expand Up @@ -729,6 +732,9 @@
BeginWorkspaceTargetTransition();
_selectedStream = value;
RequestRawStreamSelection(value?.StreamId);
ApplySelectedStreamScale(value?.StreamId);
OnPropertyChanged(nameof(SelectedEngineeringScale));
OnPropertyChanged(nameof(EngineeringScaleExplanation));
RestoreSelectedStreamWorkspace(value);
OnPropertyChanged();
OnPropertyChanged(nameof(AnalogValues));
Expand Down Expand Up @@ -843,6 +849,48 @@
public IReadOnlyList<string> WaveformTimebaseOptions { get; } = ["1 cycle", "2 cycles", "4 cycles", "8 cycles"];
public IReadOnlyList<string> WaveformModeOptions { get; } = ["Locked"];
public IReadOnlyList<string> WaveformMarkerOptions { get; } = ["On", "Off"];
public IReadOnlyList<string> EngineeringScaleOptions { get; } = [RawScaleText, NominalScaleText];
public string SelectedEngineeringScale
{
get => SelectedStream is not null &&
_explicitSvScales.TryGetValue(SelectedStream.StreamId, out var mode) &&
mode == SvEngineeringScale.Nominal4I4V ? NominalScaleText : RawScaleText;
set
{
if (SelectedStream is null || (value != RawScaleText && value != NominalScaleText))
return;
var next = value == NominalScaleText ? SvEngineeringScale.Nominal4I4V : SvEngineeringScale.RawCounts;
var key = SelectedStream.StreamId;
if (_explicitSvScales.TryGetValue(key, out var previous) ? previous == next : next == SvEngineeringScale.RawCounts)
return;

if (next == SvEngineeringScale.RawCounts) _explicitSvScales.Remove(key);
else _explicitSvScales[key] = next;
ApplySelectedStreamScale(key);
// No stale physical metering, vectors, waveform, or wrong-stream cached state.
_streamWorkspaces.Remove(key);
RestoreSelectedStreamWorkspace(SelectedStream);
OnPropertyChanged();
OnPropertyChanged(nameof(EngineeringScaleExplanation));
RaiseAnalyzerRenderProperties();
OnPropertyChanged(nameof(EvidenceSnapshotText));
}
}

public string EngineeringScaleExplanation => SelectedStream is null
? "Select an SV stream to see its measurement units."
: SelectedEngineeringScale == NominalScaleText
? "Operator-selected 4I+4V nominal scaling; not calibrated or independently verified. Applies to this stream only."
: "Uncalibrated raw counts: waveform/angles remain available; physical power is unavailable.";

private void ApplySelectedStreamScale(string? streamId)
{
if (string.IsNullOrWhiteSpace(streamId)) return;
var mode = _explicitSvScales.TryGetValue(streamId, out var selected)
? selected : SvEngineeringScale.RawCounts;
_rawDataSource.SetSvEngineeringScale(streamId, mode);
}

public IReadOnlyList<string> WaveformVoltageScaleOptions { get; } =
[
"Auto",
Expand Down Expand Up @@ -1266,7 +1314,7 @@
get
{
var power = ComputeThreePhasePower();
if (power.S <= 0)
if (!double.IsFinite(power.S) || power.S <= 0)
return "N/A";

return $"{Math.Clamp(power.P / power.S, -1.0, 1.0):0.000}";
Expand Down Expand Up @@ -2017,6 +2065,9 @@
// first-seen while the UI workspace points elsewhere. Reasserting selection before
// each snapshot is cheap and prevents the "phasor updates only after clicking SV" bug.
_rawDataSource.SelectStream(streamId);
// Re-assert operator-owned scale after a capture reset/reconnect too.
// This is an idempotent per-stream configuration, not a per-packet action.
ApplySelectedStreamScale(streamId);
_lastRequestedRawStreamId = streamId;
}

Expand Down Expand Up @@ -2077,7 +2128,10 @@
return;

_selectedStream = row;
ApplySelectedStreamScale(row.StreamId);
OnPropertyChanged(nameof(SelectedStream));
OnPropertyChanged(nameof(SelectedEngineeringScale));
OnPropertyChanged(nameof(EngineeringScaleExplanation));
}

private void RestoreSelectedStreamWorkspace(SvStreamItem? stream)
Expand Down Expand Up @@ -2392,6 +2446,8 @@
OnPropertyChanged(nameof(SvSemanticChannelSummaryText));
OnPropertyChanged(nameof(SvSemanticChannelCompactText));
OnPropertyChanged(nameof(AnalogValues));
OnPropertyChanged(nameof(SelectedEngineeringScale));
OnPropertyChanged(nameof(EngineeringScaleExplanation));
OnPropertyChanged(nameof(TotalActivePowerText));
OnPropertyChanged(nameof(TotalReactivePowerText));
OnPropertyChanged(nameof(TotalApparentPowerText));
Expand Down Expand Up @@ -3688,6 +3744,10 @@
builder.AppendLine();
builder.AppendLine("[Observed SV]");
builder.AppendLine($"Stream status: {Diagnostics.StreamStatusText}");
builder.AppendLine($"Engineering scaling: {SelectedEngineeringScale}");
builder.AppendLine(EngineeringScaleExplanation);
builder.AppendLine("Scaling authority: operator explicit selection only; SV packet shape/svID is not calibration evidence.");
builder.AppendLine($"RMS Ia: {AnalogValues.Ia.RmsDisplayText}; Ua: {AnalogValues.Ua.RmsDisplayText}; P: {TotalActivePowerText}; Q: {TotalReactivePowerText}; S: {TotalApparentPowerText}; PF: {PowerFactorText}");
builder.AppendLine($"svID: {selectedStream?.SvId ?? "N/A"}");
builder.AppendLine($"APPID: {selectedStream?.AppId ?? "N/A"}");
builder.AppendLine($"Source MAC: {selectedStream?.SourceMac ?? "N/A"}");
Expand Down Expand Up @@ -3932,38 +3992,14 @@

private (double P, double Q, double S) ComputeThreePhasePower()
{
var phases = new[]
{
(U: AnalogValues.Ua, I: AnalogValues.Ia),
(U: AnalogValues.Ub, I: AnalogValues.Ib),
(U: AnalogValues.Uc, I: AnalogValues.Ic)
};

var p = 0.0;
var q = 0.0;
var s = 0.0;

foreach (var phase in phases)
{
if (!phase.U.RmsValue.HasValue || !phase.I.RmsValue.HasValue)
continue;

var apparent = Math.Abs(phase.U.RmsValue.Value * phase.I.RmsValue.Value);
s += apparent;

if (!phase.U.AngleDegrees.HasValue || !phase.I.AngleDegrees.HasValue)
continue;

var angle = (phase.U.AngleDegrees.Value - phase.I.AngleDegrees.Value) * Math.PI / 180.0;
p += apparent * Math.Cos(angle);
q += apparent * Math.Sin(angle);
}

return (p, q, s);
return SvPhysicalPowerMeter.TryCompute(AnalogValues, out var power)
? (power.P, power.Q, power.S)
: (double.NaN, double.NaN, double.NaN);
}

private static string FormatPower(double value, string unit)
{
if (!double.IsFinite(value)) return "N/A (scaling not verified)";
var abs = Math.Abs(value);
if (abs >= 1_000_000.0)
return $"{value / 1_000_000.0:0.00} M{unit}";
Expand Down Expand Up @@ -4272,7 +4308,7 @@
public double? DeltaMs { get; init; }
public string SemanticChangedSummaryText { get; init; } = string.Empty;

public string TimeText => Source.LastSeenUtc.ToString("HH:mm:ss.fff");

Check warning on line 4311 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'DateTime.ToString(string)' could vary based on the current user's locale settings. Replace this call in 'GooseTrafficRow.TimeText.get' with a call to 'DateTime.ToString(string, IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
public string DeltaText => DeltaMs.HasValue ? $"{DeltaMs.Value:0.0}" : "-";

public string GoId => Source.GoId;
Expand Down Expand Up @@ -4317,11 +4353,11 @@
}
}
public string AppId => Source.AppId;
public string EntryTimeText => Source.LastSeenUtc.ToLocalTime().ToString("M/d/yyyy h:mm:ss.fff tt");

Check warning on line 4356 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'DateTime.ToString(string)' could vary based on the current user's locale settings. Replace this call in 'GooseTrafficRow.EntryTimeText.get' with a call to 'DateTime.ToString(string, IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
public string NeedsCommissionText => Source.NeedsCommission ? "true" : "false";
public string TestText => Source.IsTest ? "true" : "false";
public int DatasetEntryCount => Source.DataValues.Count;
public string DatasetEntryCountText => DatasetEntryCount.ToString();

Check warning on line 4360 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'int.ToString()' could vary based on the current user's locale settings. Replace this call in 'GooseTrafficRow.DatasetEntryCountText.get' with a call to 'int.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
public string DestinationMac => Source.DestinationMac;
public string SourceMac => Source.SourceMac;
public string VlanId => Source.VlanId;
Expand All @@ -4341,7 +4377,7 @@
return Array.Empty<string>();

var trimmed = valuesText.Trim();
if (trimmed.StartsWith("[", StringComparison.Ordinal) && trimmed.EndsWith("]", StringComparison.Ordinal))

Check warning on line 4380 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

Use 'string.StartsWith(char)' instead of 'string.StartsWith(string)' when you have a string with a single char (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1865)
trimmed = trimmed[1..^1];

return trimmed
Expand Down Expand Up @@ -4457,7 +4493,7 @@
var age = DateTime.UtcNow - LastSeenUtc.Value;
return age.TotalSeconds < 60
? $"last {age.TotalSeconds:0.0}s"
: LastSeenUtc.Value.ToLocalTime().ToString("HH:mm:ss");

Check warning on line 4496 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'DateTime.ToString(string)' could vary based on the current user's locale settings. Replace this call in 'TrafficHealthTargetRow.LastSeenText.get' with a call to 'DateTime.ToString(string, IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
}
}

Expand Down Expand Up @@ -4538,7 +4574,7 @@
public string EntryCountText => $"{Entries.Count} entry(s)";
public string SummaryText => $"{IedName} - {TransportText}";
public string ExpectedKey => $"{Protocol}|{SourceFileName}|{IedName}|{ControlBlockReference}|{DisplayName}";
public string ExpectedConfRevText => ConfRev > 0 ? ConfRev.ToString() : "N/A";

Check warning on line 4577 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'int.ToString()' could vary based on the current user's locale settings. Replace this call in 'SclStreamCatalogRow.ExpectedConfRevText.get' with a call to 'int.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

public static SclStreamCatalogRow FromSv(string sourceFileName, SclSvStreamModel stream, string liveStatus)
=> new()
Expand Down Expand Up @@ -4767,7 +4803,7 @@
return ("No evidence captured yet", string.Empty);

var parts = evidence
.Split(new[] { "; " }, StringSplitOptions.RemoveEmptyEntries)

Check warning on line 4806 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861)
.Select(part => part.Trim())
.Where(part => part.Length > 0)
.ToArray();
Expand Down
30 changes: 30 additions & 0 deletions src/ProcessBus.Core/Models/SvEngineeringScale.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace ProcessBus.Core.Models;

/// <summary>
/// Explicit scale authority for a selected SV stream. Packet shape, svID, APPID,
/// and a missing SCL are NOT evidence of engineering calibration.
/// </summary>
public enum SvEngineeringScale
{
RawCounts = 0,
Nominal4I4V = 1
}

public static class SvEngineeringScaleFactors
{
// This explicitly opted-in 4I+4V lab/nominal profile is 1 mA per
// current count and 10 mV per voltage count. It is NOT universal
// IEC 61850 SV scaling and must never be auto-selected by svID.
public static double Factor(SvEngineeringScale mode, string channel)
=> mode == SvEngineeringScale.Nominal4I4V
? (channel.StartsWith("U", StringComparison.OrdinalIgnoreCase) ? 0.01 : 0.001)
: 1.0;

public static string Unit(SvEngineeringScale mode, string channel)
=> mode != SvEngineeringScale.Nominal4I4V
? "counts"
: channel.StartsWith("U", StringComparison.OrdinalIgnoreCase) ? "V" : "A";

public static bool IsPhysical(SvEngineeringScale mode)
=> mode == SvEngineeringScale.Nominal4I4V;
}
42 changes: 42 additions & 0 deletions src/ProcessBus.Core/Models/SvPhysicalPowerMeter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace ProcessBus.Core.Models;

// Physical watts/vars require real A/V units for every phase in one
// selected-stream coherent snapshot. Raw INT32 counts never qualify.
public readonly record struct SvPhysicalPowerTotals(double P, double Q, double S);

public static class SvPhysicalPowerMeter
{
public static bool TryCompute(AnalogValuesSnapshot snapshot, out SvPhysicalPowerTotals totals)
{
totals = default;
if (snapshot is null) return false;
var phases = new[]
{
(U: snapshot.Ua, I: snapshot.Ia),
(U: snapshot.Ub, I: snapshot.Ib),
(U: snapshot.Uc, I: snapshot.Ic)
};
var p = 0.0;
var q = 0.0;
var s = 0.0;
foreach (var (u, i) in phases)
{
if (u.Unit != "V" || i.Unit != "A" ||
u.RmsValue is not { } uRms || i.RmsValue is not { } iRms ||
u.AngleDegrees is not { } uAngle || i.AngleDegrees is not { } iAngle ||
!double.IsFinite(uRms) || !double.IsFinite(iRms) ||
!double.IsFinite(uAngle) || !double.IsFinite(iAngle) ||
uRms < 0 || iRms < 0)
return false;

var apparent = uRms * iRms;
var delta = (uAngle - iAngle) * Math.PI / 180.0;
p += apparent * Math.Cos(delta);
q += apparent * Math.Sin(delta);
s += apparent;
}
if (!double.IsFinite(p) || !double.IsFinite(q) || !double.IsFinite(s)) return false;
totals = new SvPhysicalPowerTotals(p, q, s);
return true;
}
}
1 change: 1 addition & 0 deletions src/ProcessBus.Core/Services/IRawCaptureDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public interface IRawCaptureDataSource : IAnalyzerDataSource
void SelectAdapter(string adapterId, string rawDeviceName);
void SelectStream(string? streamId);
void SetScopeCycles(int cycles);
void SetSvEngineeringScale(string streamId, SvEngineeringScale scale);
void SetSvChannelMappings(IReadOnlyList<SvChannelMappingProfile> profiles);
Task StartAsync(CancellationToken cancellationToken = default);
Task StopAsync(CancellationToken cancellationToken = default);
Expand Down
Loading
Loading