Skip to content
Closed
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
23 changes: 23 additions & 0 deletions Services/NativeIec61850Client.CanonicalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public sealed partial class NativeIec61850Client
{
private LiveIedCanonicalModel? _liveCanonicalModel;
private ArMms.InitialFcReadExecutionResult? _liveInitialFcRead;
private LiveIedModelDiscoveryDocument? _saveSclEnrichmentModel;
private DateTimeOffset _saveSclEnrichmentCapturedAtUtc = DateTimeOffset.MinValue;
private static readonly TimeSpan SaveSclEnrichmentReuseWindow = TimeSpan.FromSeconds(15);

public bool LastSaveSclEnrichmentReused { get; private set; }

/// <summary>
/// Canonical live snapshot bound to the accepted MMS association that produced
Expand Down Expand Up @@ -56,6 +61,19 @@ public async Task<LiveIedCanonicalModel> EnrichCanonicalForSclSaveAsync(
if (_liveModel is null)
throw new InvalidOperationException("Save-SCL enrichment requires a successful live discovery model.");

var now = DateTimeOffset.UtcNow;
if (_liveCanonicalModel is not null &&
_liveInitialFcRead is not null &&
ReferenceEquals(_saveSclEnrichmentModel, _liveModel) &&
_saveSclEnrichmentCapturedAtUtc != DateTimeOffset.MinValue &&
now - _saveSclEnrichmentCapturedAtUtc <= SaveSclEnrichmentReuseWindow)
{
LastSaveSclEnrichmentReused = true;
return _liveCanonicalModel;
}

LastSaveSclEnrichmentReused = false;

var sourcePlan = ArMms.InitialFcReadPlanner.FromSclModel(_liveModel);
var safeTargets = sourcePlan.Targets
.Where(target => SaveSclEnrichmentFunctionalConstraints.Contains(
Expand Down Expand Up @@ -92,6 +110,8 @@ or ArMms.InitialFcReadExecutionStatus.TimedOut
}

PublishCanonicalModel(_liveModel, initialRead);
_saveSclEnrichmentModel = _liveModel;
_saveSclEnrichmentCapturedAtUtc = DateTimeOffset.UtcNow;
return _liveCanonicalModel
?? throw new InvalidOperationException("Save-SCL enrichment did not publish a canonical model.");
}
Expand All @@ -100,5 +120,8 @@ private void ClearCanonicalModel()
{
_liveInitialFcRead = null;
_liveCanonicalModel = null;
_saveSclEnrichmentModel = null;
_saveSclEnrichmentCapturedAtUtc = DateTimeOffset.MinValue;
LastSaveSclEnrichmentReused = false;
}
}
19 changes: 19 additions & 0 deletions tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ public void NativeClient_RetainsAcceptedAssociationCanonicalSnapshot()
Assert.Contains("ClearCanonicalModel();", lifecycle, StringComparison.Ordinal);
}

[Fact]
public void SaveSclEnrichment_ReusesOnlyShortLivedSameModelSnapshot()
{
var canonical = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.CanonicalModel.cs"));
var lifecycle = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SmartDiscoveryLifecycle.cs"));
var client = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.cs"));

Assert.Contains("SaveSclEnrichmentReuseWindow = TimeSpan.FromSeconds(15)", canonical, StringComparison.Ordinal);
Assert.Contains("ReferenceEquals(_saveSclEnrichmentModel, _liveModel)", canonical, StringComparison.Ordinal);
Assert.Contains("now - _saveSclEnrichmentCapturedAtUtc <= SaveSclEnrichmentReuseWindow", canonical, StringComparison.Ordinal);
Assert.Contains("LastSaveSclEnrichmentReused = true", canonical, StringComparison.Ordinal);
Assert.Contains("_saveSclEnrichmentModel = _liveModel", canonical, StringComparison.Ordinal);
Assert.Contains("_saveSclEnrichmentCapturedAtUtc = DateTimeOffset.UtcNow", canonical, StringComparison.Ordinal);
Assert.Contains("_saveSclEnrichmentModel = null", canonical, StringComparison.Ordinal);
Assert.Contains("_saveSclEnrichmentCapturedAtUtc = DateTimeOffset.MinValue", canonical, StringComparison.Ordinal);
Assert.Contains("ClearCanonicalModel();", lifecycle, StringComparison.Ordinal);
Assert.Contains("ClearCanonicalModel();", client, StringComparison.Ordinal);
}

[Fact]
public void SmartDiscovery_DefersEagerFcValueReadsFromStructuralScan()
{
Expand Down
Loading