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
193 changes: 193 additions & 0 deletions MainWindow.Demo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System.Globalization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ArIED61850Tester.Models;

namespace ArIED61850Tester;

public partial class MainWindow
{
private const string DemoShortcutText = "Ctrl+Shift+D";
private readonly DispatcherTimer _demoTimer = new(DispatcherPriority.Background);
private readonly Random _demoRandom = new(61850);
private readonly List<DemoPointState> _demoPointStates = new();
private readonly List<DemoGooseStreamState> _demoGooseStates = new();
private bool _isDemoMode;
private int _demoTick;
private long _demoEventSequence = 5000;

public bool IsDemoMode => _isDemoMode;
public Visibility DemoModeVisibility => _isDemoMode ? Visibility.Visible : Visibility.Collapsed;
public string DemoModeText => $"DEMO MODE • {DemoShortcutText}";

private void MainWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.Modifiers != (ModifierKeys.Control | ModifierKeys.Shift) || e.Key != Key.D)
return;

e.Handled = true;
ToggleDemoMode();
}

private void ToggleDemoMode()
{
if (_isDemoMode)
{
DeactivateDemoMode();
return;
}

if (Devices.Any(device => device.IsConnected || device.IsMonitoring || device.IsBusy) || IsGooseCapturing)
{
MessageBox.Show(
this,
"Disconnect every live IED and stop GOOSE capture before entering Demo Mode.",
"Demo Mode",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}

if (Devices.Count > 0 || GlobalPoints.Count > 0 || Events.Count > 0)
{
var choice = MessageBox.Show(
this,
"Demo Mode replaces the current offline workspace with synthetic substation data. Save the current project first if it must be retained.\n\nContinue?",
"Load Demo Workspace",
MessageBoxButton.YesNo,
MessageBoxImage.Warning,
MessageBoxResult.No);
if (choice != MessageBoxResult.Yes)
return;
}

ActivateDemoMode();
}

private void ActivateDemoMode()
{
ClearWorkspaceForDemo();
_isDemoMode = true;
_demoTick = 0;
_demoEventSequence = 5000;
Title = "ArIED 61850 — Synthetic Substation Demo";

InstallGoosePresentationWorkspace();
BuildDemoDevicesAndLivePoints();
BuildDemoEventHistory();
BuildDemoGooseWorkspace();
BuildDemoDiagnostics();

SelectedDevice = Devices.FirstOrDefault();
MainTabs.SelectedIndex = 0;
UpdateNavigationVisuals(0, animate: false);

_demoTimer.Stop();
_demoTimer.Interval = TimeSpan.FromMilliseconds(1100);
_demoTimer.Tick -= DemoTimer_Tick;
_demoTimer.Tick += DemoTimer_Tick;
_demoTimer.Start();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block live workspace actions while demo is running

While the demo timer is running, the always-visible Add IED/Open SCL/Open Project controls remain enabled, and Add IED reaches AddOrDiscoverEndpointAsync → ConnectAndConfigureDeviceAsync → _runtime.ConnectAndDiscoverAsync. That lets a user start real MMS sessions inside the advertised read-only demo; subsequent demo ticks still mutate events/status from the old synthetic state, and exiting demo clears Devices without stopping any runtime session added during demo. Guard workspace-mutating actions with IsDemoMode or force demo deactivation before they run.

Useful? React with 👍 / 👎.


Raise(nameof(IsDemoMode));
Raise(nameof(DemoModeVisibility));
Raise(nameof(DemoModeText));
RaiseWorkspaceCounts();
SetStatus($"DEMO MODE active • 10 synthetic IEDs • {GlobalPoints.Count:N0} live values • {GooseStreams.Count:N0} GOOSE publishers • press {DemoShortcutText} to exit.");
}

private void DeactivateDemoMode()
{
_demoTimer.Stop();
IsGooseCapturing = false;
_isDemoMode = false;
Title = "ArIED 61850 — Smart IED Explorer & Monitor";
ClearWorkspaceForDemo();
AddLog("INFO", "Demo", "Synthetic substation demo closed. No network sessions were created.");
Raise(nameof(IsDemoMode));
Raise(nameof(DemoModeVisibility));
Raise(nameof(DemoModeText));
RaiseWorkspaceCounts();
SetStatus($"Demo workspace cleared. Press {DemoShortcutText} to load it again.");
}

private void ClearWorkspaceForDemo()
{
_demoTimer.Stop();
SelectedDevice = null;

foreach (var device in Devices)
DetachSignalHandlers(device.Signals);

Devices.Clear();
GlobalPoints.Clear();
Events.Clear();
Logs.Clear();
_signalOwners.Clear();
_pointIndex.Clear();
_controlFeedbackIndex.Clear();
_pendingPointSnapshots.Clear();
_reportPulseUntil.Clear();
_pointHighlightUntil.Clear();
while (_pendingEvents.TryDequeue(out _)) { }
while (_pendingDiagnostics.TryDequeue(out _)) { }

_demoPointStates.Clear();
_demoGooseStates.Clear();
ResetGooseView(resetCounters: true);
ResetGooseTimelineUi();
GooseAdapters.Clear();
SelectedGooseAdapter = null;
}

private void BuildDemoDevicesAndLivePoints()
{
var deviceSpecs = new[]
{
new DemoDeviceSpec("BCU_INCOMER_150KV", "BCU • 150 kV line incomer", "192.168.10.11", "3 LD • 29 LN • 186 DO • 1,042 DA", DemoDeviceRole.Bcu),
new DemoDeviceSpec("BCU_TRAFO_BAY_TR1", "BCU • transformer HV bay", "192.168.10.12", "3 LD • 31 LN • 194 DO • 1,108 DA", DemoDeviceRole.Bcu),
new DemoDeviceSpec("PROT_OCR_INCOMER_20KV", "OCR/GFR • 20 kV incomer", "192.168.10.21", "2 LD • 26 LN • 171 DO • 936 DA", DemoDeviceRole.Ocr),
new DemoDeviceSpec("PROT_OCR_FEEDER_F01", "OCR/GFR • 20 kV feeder", "192.168.10.22", "2 LD • 24 LN • 158 DO • 874 DA", DemoDeviceRole.Ocr),
new DemoDeviceSpec("PROT_LINE_DIFF_L01", "87L line differential", "192.168.10.31", "3 LD • 34 LN • 221 DO • 1,294 DA", DemoDeviceRole.LineDiff),
new DemoDeviceSpec("PROT_DISTANCE_L02", "21 distance protection", "192.168.10.32", "3 LD • 33 LN • 216 DO • 1,247 DA", DemoDeviceRole.Distance),
new DemoDeviceSpec("PROT_TRAFO_DIFF_TR1", "87T transformer differential", "192.168.10.41", "3 LD • 38 LN • 248 DO • 1,462 DA", DemoDeviceRole.TrafoDiff),
new DemoDeviceSpec("PROT_BUSBAR_DIFF_BB1", "87B busbar differential", "192.168.10.51", "4 LD • 46 LN • 312 DO • 1,856 DA", DemoDeviceRole.BusbarDiff),
new DemoDeviceSpec("PROT_CAPBANK_CB1", "capacitor-bank protection", "192.168.10.61", "2 LD • 25 LN • 164 DO • 902 DA", DemoDeviceRole.CapBank),
new DemoDeviceSpec("BCU_BUS_COUPLER_150KV", "BCU • 150 kV bus coupler", "192.168.10.71", "3 LD • 30 LN • 191 DO • 1,081 DA", DemoDeviceRole.Coupler)
};

for (var deviceIndex = 0; deviceIndex < deviceSpecs.Length; deviceIndex++)
{
var spec = deviceSpecs[deviceIndex];
var device = new Iec61850MonitorDevice
{
DeviceId = $"demo-{deviceIndex + 1:00}-{spec.Name.ToLowerInvariant()}",
Name = spec.Name,
IdentitySource = $"DEMO • live MMS discovery • {spec.Description}",
LogicalDeviceSummary = spec.ModelSummary,
IpAddress = spec.IpAddress,
Port = 102,
Status = "Monitoring • BRCB active",
Detail = $"Synthetic {spec.Description} session. Buffered reports, MMS validation reads and GOOSE bindings are generated locally; no packets are transmitted.",
AcquisitionMode = deviceIndex % 3 == 0
? "Buffered Report • dchg/qchg/dupd"
: deviceIndex % 3 == 1
? "Unbuffered Report • dchg + integrity"
: "Static DataSet report • MMS verification",
HasDiscoveryCache = true,
IsConnected = true,
IsMonitoring = true,
HasReportStream = true,
IsDemo = true
};

var seeds = BuildDemoSignalSeeds(spec.Role, deviceIndex);
foreach (var seed in seeds)
AddDemoPoint(device, seed, deviceIndex);

device.RecountSelectedSignals();
device.AddUnreadEvents(1 + deviceIndex % 4);
Devices.Add(device);
}
}
}
122 changes: 122 additions & 0 deletions MainWindow.DemoGooseData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.Globalization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ArIED61850Tester.Models;

namespace ArIED61850Tester;

public partial class MainWindow
{
private void BuildDemoGooseWorkspace()
{
GooseAdapters.ReplaceAll(new[]
{
new GooseAdapterOption
{
Index = 1,
Name = "demo://station-bus",
FriendlyName = "Station Bus VLAN 100 (Synthetic)",
Description = "ArIED built-in demonstration adapter",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep demo GOOSE state separate from live capture controls

In demo mode, if a user clicks GOOSE Stop and then Start, this reuses the real IsGooseCapturing flag even though _gooseSubscriberRuntime.StartAsync was never called for the synthetic data. StopGooseSubscriberAsync sets the flag back to false, which enables Start; StartGooseSubscriber_Click then uses the demo adapter's Selector (Index = 1) and can start an actual Npcap capture on adapter 1 while demo mode is still active. Use a demo-only capture state or make the GOOSE start/stop commands ignore demo mode.

Useful? React with 👍 / 👎.

MacAddress = "02:00:5E:61:85:00"
}
});
SelectedGooseAdapter = GooseAdapters[0];
IsGooseCapturing = true;
GooseBindingText = "DEMO model binding • 6 publishers • ordered DataSet leaves resolved from synthetic IED models";
GooseStatusText = "DEMO • synthetic read-only GOOSE capture is running; no Ethernet frames are transmitted or received.";

var streamSpecs = new[]
{
DemoGooseSpec("gcb-incomer", "BCU_INCOMER_150KV", "0x1001", "01:0C:CD:01:00:01", 100, "gcbTripStatus", "dsTripStatus",
("Breaker position", "XCBR1.Pos.stVal", "DPC / Dbpos", "Closed [10]"),
("Master trip", "PTRC1.Tr.general", "ACT / BOOLEAN", "false"),
("Close interlock release", "CILO1.EnaCls.stVal", "SPS / BOOLEAN", "true"),
("Control authority", "CSWI1.Loc.stVal", "ENS / INT32", "Remote")),
DemoGooseSpec("gcb-linediff", "PROT_LINE_DIFF_L01", "0x1101", "01:0C:CD:01:01:01", 110, "gcbLineProtection", "dsLineProtection",
("Line differential pickup", "PDIF1.Str.general", "ACD / BOOLEAN", "false"),
("Line differential trip", "PDIF1.Op.general", "ACT / BOOLEAN", "false"),
("Teleprotection receive", "PSCH1.Op.general", "ACT / BOOLEAN", "false"),
("Breaker position", "XCBR1.Pos.stVal", "DPC / Dbpos", "Closed [10]")),
DemoGooseSpec("gcb-trafodiff", "PROT_TRAFO_DIFF_TR1", "0x1201", "01:0C:CD:01:02:01", 120, "gcbTransformerTrip", "dsTransformerTrip",
("Transformer differential pickup", "PDIF1.Str.general", "ACD / BOOLEAN", "false"),
("Transformer differential trip", "PDIF1.Op.general", "ACT / BOOLEAN", "false"),
("Inrush restraint", "PHAR1.Str.general", "ACD / BOOLEAN", "false"),
("HV breaker position", "XCBR1.Pos.stVal", "DPC / Dbpos", "Closed [10]")),
DemoGooseSpec("gcb-busbar", "PROT_BUSBAR_DIFF_BB1", "0x1301", "01:0C:CD:01:03:01", 130, "gcbBusbarTrip", "dsBusbarTrip",
("Busbar differential pickup", "PDIF1.Str.general", "ACD / BOOLEAN", "false"),
("Busbar zone 1 trip", "PDIF1.Op.general", "ACT / BOOLEAN", "false"),
("Breaker-failure initiate", "RBRF1.OpEx.general", "ACT / BOOLEAN", "false"),
("Zone healthy", "GGIO1.Ind1.stVal", "SPS / BOOLEAN", "true")),
DemoGooseSpec("gcb-coupler", "BCU_BUS_COUPLER_150KV", "0x1401", "01:0C:CD:01:04:01", 140, "gcbCouplerInterlock", "dsCouplerInterlock",
("Coupler position", "XCBR1.Pos.stVal", "DPC / Dbpos", "Open [01]"),
("Synchrocheck release", "RSYN1.Rel.stVal", "SPS / BOOLEAN", "true"),
("Bus 1 voltage healthy", "GGIO1.Ind2.stVal", "SPS / BOOLEAN", "true"),
("Bus 2 voltage healthy", "GGIO1.Ind3.stVal", "SPS / BOOLEAN", "true")),
DemoGooseSpec("gcb-feeder", "PROT_OCR_FEEDER_F01", "0x1501", "01:0C:CD:01:05:01", 150, "gcbFeederProtection", "dsFeederProtection",
("Overcurrent pickup", "PTOC1.Str.general", "ACD / BOOLEAN", "false"),
("Earth-fault pickup", "PTOC3.Str.general", "ACD / BOOLEAN", "false"),
("Master trip", "PTRC1.Tr.general", "ACT / BOOLEAN", "false"),
("Breaker position", "XCBR1.Pos.stVal", "DPC / Dbpos", "Closed [10]"))
};

var baseTime = DateTimeOffset.Now.AddMinutes(-4);
for (var index = 0; index < streamSpecs.Length; index++)
{
var state = new DemoGooseStreamState(streamSpecs[index], stateNumber: 12 + index * 3, sequenceNumber: 510 + index * 71, packetCount: 8200 + index * 1260);
var row = new GooseStreamRow { StreamKey = state.Spec.StreamKey };
state.Row = row;
row.Apply(BuildDemoGooseSnapshot(state, changedIndex: -1, diagnostics: string.Empty, baseTime.AddSeconds(index * 4)));
GooseStreams.Add(row);
_gooseStreamIndex[row.StreamKey] = row;
_demoGooseStates.Add(state);
}

for (var index = 0; index < 30; index++)
{
var state = _demoGooseStates[index % _demoGooseStates.Count];
var timestamp = baseTime.AddSeconds(index * 8.2);
var leaf = state.Leaves[index % state.Leaves.Count];
GooseEvents.Add(new GooseEventRow
{
StreamKey = state.Spec.StreamKey,
Timestamp = timestamp,
DeltaText = index < _demoGooseStates.Count ? "-" : "8.200 s",
EventText = index % 11 == 0 ? "Warning" : "State change",
EventTone = index % 11 == 0 ? "Warning" : "Change",
Publisher = state.Spec.IedName,
StateSequenceText = $"{state.StateNumber + index / _demoGooseStates.Count} / 0",
Summary = index % 11 == 0
? "TAL supervision recovered after one delayed retransmission"
: $"{leaf.Name}: {ToggleDemoValue(leaf.Value)}"
});
}

SelectedGooseStream = GooseStreams.FirstOrDefault();
SelectedGooseEvent = GooseEvents.LastOrDefault();
GooseCapturedFrames = _demoGooseStates.Sum(state => state.PacketCount) + 412;
GooseFrames = GooseCapturedFrames;
GooseOtherFrames = 0;
Raise(nameof(GooseCounterText));
Raise(nameof(GooseNoStreamsVisibility));
Raise(nameof(GooseNoLeafValuesVisibility));
RaiseGoosePresentationState();
}

private void BuildDemoDiagnostics()
{
var now = DateTime.Now;
var logs = new List<DiagnosticEntry>
{
new() { Time = now.AddMinutes(-8), Level = "INFO", Source = "Demo", Message = "Synthetic substation workspace initialized. Network transport is disabled." },
new() { Time = now.AddMinutes(-7.8), Level = "INFO", Source = "MMS", Message = "10 independent IEC 61850 associations established on TCP/102 (synthetic)." },
new() { Time = now.AddMinutes(-7.6), Level = "INFO", Source = "Discovery", Message = "IEDName, LD/LN/DO/DA, DataSets, RCBs and GSEControl models resolved for all demo IEDs." },
new() { Time = now.AddMinutes(-7.4), Level = "INFO", Source = "Reporting", Message = "6 BRCB and 4 URCB sessions enabled; dchg/qchg/dupd and integrity reasons validated." },
new() { Time = now.AddMinutes(-7.2), Level = "INFO", Source = "GOOSE", Message = "6 station-bus publishers bound to ordered DataSet leaves on VLANs 100-150." },
new() { Time = now.AddMinutes(-4.1), Level = "WARN", Source = "GOOSE", Message = "One delayed retransmission exceeded the expected interval; TAL supervision recovered without state loss." },
new() { Time = now.AddMinutes(-3.9), Level = "INFO", Source = "GOOSE", Message = "Publisher sequence returned to normal; stNum and sqNum continuity verified." },
new() { Time = now.AddMinutes(-1.2), Level = "INFO", Source = "Reporting", Message = "Periodic integrity report completed for PROT_TRAFO_DIFF_TR1; 12 values confirmed good." }
};
Logs.AddRange(logs);
}
}
70 changes: 70 additions & 0 deletions MainWindow.DemoGooseSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Globalization;
using ArIED61850Tester.Models;

namespace ArIED61850Tester;

public partial class MainWindow
{
private GooseStreamSnapshot BuildDemoGooseSnapshot(DemoGooseStreamState state, int changedIndex, string diagnostics, DateTimeOffset timestamp)
{
var leaves = state.Leaves.Select((leaf, index) => new GooseLeafValueSnapshot(
index + 1,
index,
leaf.Name,
$"{state.Spec.IedName}LD0/{leaf.Path}",
leaf.Path.Contains(".mag.", StringComparison.OrdinalIgnoreCase) ? "MX" : "ST",
leaf.TypeText.Split('/')[0].Trim(),
leaf.TypeText.Contains('/') ? leaf.TypeText.Split('/')[1].Trim() : string.Empty,
leaf.Value,
index == changedIndex ? leaf.PreviousValue : leaf.Value,
index == changedIndex,
"DEMO model")).ToArray();

return new GooseStreamSnapshot(
state.Spec.StreamKey,
state.Spec.AppId,
$"{state.Spec.IedName}LD0/LLN0$GO${state.Spec.ControlBlock}",
$"{state.Spec.IedName}_{state.Spec.ControlBlock}",
$"{state.Spec.IedName}LD0/LLN0$DataSet${state.Spec.DataSet}",
state.Spec.SourceMac,
state.Spec.DestinationMac,
$"VID {state.Spec.Vlan} / PCP 4",
state.StateNumber.ToString(CultureInfo.InvariantCulture),
state.SequenceNumber.ToString(CultureInfo.InvariantCulture),
changedIndex >= 0 ? "StateChange" : "Retransmission",
"2000 ms",
"1",
state.Spec.IedName,
"DEMO model",
diagnostics,
timestamp.ToLocalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture),
state.PacketCount,
changedIndex >= 0 ? 1 : 0,
false,
false,
leaves);
}

private static DemoGooseStreamSpec DemoGooseSpec(
string streamKey,
string iedName,
string appId,
string destinationMac,
int vlan,
string controlBlock,
string dataSet,
params (string Name, string Path, string TypeText, string InitialValue)[] leaves)
{
var lastOctet = (Math.Abs((long)iedName.GetHashCode(StringComparison.Ordinal)) % 180) + 20;
return new DemoGooseStreamSpec(
streamKey,
iedName,
appId,
$"02:00:5E:61:85:{lastOctet:X2}",
destinationMac,
vlan,
controlBlock,
dataSet,
leaves.Select(leaf => new DemoGooseLeafState(leaf.Name, leaf.Path, leaf.TypeText, leaf.InitialValue)).ToArray());
}
}
Loading
Loading