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
4 changes: 2 additions & 2 deletions .release/windows.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "1.6.29",
"version": "1.6.30",
"channel": "stable",
"platform": "windows-x64",
"primaryAsset": "ARSAS-Windows-x64-Setup.exe",
"publicationRequest": 17
"publicationRequest": 18
}
6 changes: 3 additions & 3 deletions ArIED61850Tester.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<Product>ARSAS</Product>
<AssemblyTitle>ARSAS - IEC 61850 Engineering Workstation</AssemblyTitle>
<Description>Open-source Windows IEC 61850 engineering workstation for MMS model discovery, reporting, independent multi-IED monitoring, GOOSE subscription, fault-record file transfer, Sampled Values engineering and evidence export, SCL workflows, diagnostics, sequence of events, and guarded control validation.</Description>
<Version>1.6.29</Version>
<AssemblyVersion>1.6.29.0</AssemblyVersion>
<FileVersion>1.6.29.0</FileVersion>
<Version>1.6.30</Version>
<AssemblyVersion>1.6.30.0</AssemblyVersion>
<FileVersion>1.6.30.0</FileVersion>
<PackageProjectUrl>https://github.com/masarray/arsas</PackageProjectUrl>
<RepositoryUrl>https://github.com/masarray/arsas</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ Notable public changes to ARSAS are recorded here. Application releases must ide

## Unreleased

## 1.6.30 — 2026-08-15

### Added

- ARIEC-authoritative hybrid acquisition planning and execution for static BRCB/URCB, verified-free dynamic RCB slots, and per-signal MMS polling residuals.
- Physical validation tracking that distinguishes successful RCB setup from received report frames and verified process-value changes.

### Fixed

- Live discovery now requests and retains DataSet directories, so static DataSet members are present in the signal catalog and signal inventory.
- Hybrid catalog matching now selects the owning primary-value descriptor instead of treating its related quality and timestamp descriptors as ambiguous duplicates.
- Report-planning inventory clones preserve live `DatSet`, owner, enable, and reservation probe evidence, allowing verified-empty dynamic slots to remain eligible.
- Instantaneous magnitude references such as `Hz.instMag.f` now resolve quality and timestamp companions at `Hz.q` and `Hz.t`; friendly signal names no longer corrupt `instMag` into `inStatusue`.

## 1.6.20 — 2026-08-04

### Added
Expand Down Expand Up @@ -97,4 +111,4 @@ Notable public changes to ARSAS are recorded here. Application releases must ide

## Historical licensing boundary

Revisions through `0df1007d9538b978edba67218136bc5c4f8019ad` remain available under their original terms on branch `archive/apache-2.0-final`. Current `main` and current community release packages are GPL-3.0-or-later only. See `docs/LICENSING.md`.
Revisions through `0df1007d9538b978edba67218136bc5c4f8019ad` remain available under their original terms on branch `archive/apache-2.0-final`. Current `main` and current community release packages are GPL-3.0-or-later only. See `docs/LICENSING.md`.
8 changes: 4 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<Version>1.6.29</Version>
<AssemblyVersion>1.6.29.0</AssemblyVersion>
<FileVersion>1.6.29.0</FileVersion>
<InformationalVersion>1.6.29</InformationalVersion>
<Version>1.6.30</Version>
<AssemblyVersion>1.6.30.0</AssemblyVersion>
<FileVersion>1.6.30.0</FileVersion>
<InformationalVersion>1.6.30</InformationalVersion>
</PropertyGroup>
</Project>
76 changes: 76 additions & 0 deletions Models/NativeHybridReportAcquisitionModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace ArIED61850Tester.Models;

/// <summary>
/// ARSAS projection of an ARIEC-owned hybrid report acquisition plan.
/// The application never infers report capability from these counters; they only expose
/// the typed decision already returned by the engine planner.
/// </summary>
public sealed class NativeHybridReportPlanningResult
{
public bool IsAuthoritative { get; init; }
public string Authority { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public string Summary { get; init; } = string.Empty;
public IReadOnlyList<ReportControlPlan> ReportPlans { get; init; } = Array.Empty<ReportControlPlan>();
public IReadOnlyList<string> PollingPointKeys { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> UncoveredPointKeys { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> UnmappedPointKeys { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
public int RequestedPointCount { get; init; }
public int CatalogMappedPointCount { get; init; }
public int StaticBrcbSignalCount { get; init; }
public int StaticUrcbSignalCount { get; init; }
public int DynamicBrcbSignalCount { get; init; }
public int DynamicUrcbSignalCount { get; init; }
public int PollingFallbackSignalCount { get; init; }
public int UncoveredSignalCount { get; init; }

public bool HasReportPlans => ReportPlans.Count > 0;
}

/// <summary>
/// Physical-validation evidence collected by ARSAS after executing an engine-authoritative
/// report plan against a real IED. Silence is never interpreted as signal absence.
/// </summary>
public sealed class HybridReportPhysicalValidationSnapshot
{
public DateTimeOffset CapturedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public string DeviceId { get; init; } = string.Empty;
public string DeviceName { get; init; } = string.Empty;
public string Endpoint { get; init; } = string.Empty;
public int PlannedStaticBrcbCount { get; init; }
public int PlannedStaticUrcbCount { get; init; }
public int PlannedDynamicBrcbCount { get; init; }
public int PlannedDynamicUrcbCount { get; init; }
public int ActivatedReportPlanCount { get; init; }
public int FailedActivationCount { get; init; }
public int ReportFrameCount { get; init; }
public int ReportUpdateCount { get; init; }
public int ChangeVerifiedPointCount { get; init; }
public int PollingFallbackPointCount { get; init; }
public int UncoveredPointCount { get; init; }
public IReadOnlyList<HybridReportPhysicalValidationPlan> Plans { get; init; } = Array.Empty<HybridReportPhysicalValidationPlan>();
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();

public bool HasPhysicalReportEvidence => ReportFrameCount > 0 || ReportUpdateCount > 0;
}

public sealed class HybridReportPhysicalValidationPlan
{
public string PlanId { get; init; } = string.Empty;
public string AcquisitionKind { get; init; } = string.Empty;
public string ReportControlReference { get; init; } = string.Empty;
public string DataSetReference { get; init; } = string.Empty;
public int PlannedSignalCount { get; init; }
public bool ActivationSucceeded { get; init; }
public string ActivationMessage { get; init; } = string.Empty;
public string SubscriptionSummary { get; init; } = string.Empty;
public int MemberCount { get; init; }
public int SetupWriteStepCount { get; init; }
public bool UsedDynamicDataSet { get; init; }
public int ReportFrameCount { get; init; }
public int ReportUpdateCount { get; init; }
public int ChangeVerifiedPointCount { get; init; }
public DateTimeOffset? FirstReportAtUtc { get; init; }
public DateTimeOffset? LastReportAtUtc { get; init; }
}
6 changes: 6 additions & 0 deletions Models/ReportControlPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public sealed class ReportControlPlan
public string TriggerOptions { get; set; } = string.Empty;
public string OptionalFields { get; set; } = string.Empty;
public string Status { get; set; } = "Planned";
/// <summary>
/// True when the RCB/DataSet choice and exact subscription plan were emitted by
/// ARIEC61850 MmsHybridReportAcquisitionPlanner. ARSAS executes this plan as-is.
/// </summary>
public bool IsEngineAuthoritative { get; set; }
public string EngineAcquisitionKind { get; set; } = string.Empty;
public List<Iec61850MonitorPoint> Bindings { get; set; } = new();

public int BindingCount => Bindings.Count;
Expand Down
3 changes: 3 additions & 0 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ARSAS.Tests")]
166 changes: 166 additions & 0 deletions Services/HybridReportPhysicalValidationTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using ArIED61850Tester.Models;

namespace ArIED61850Tester.Services;

/// <summary>
/// Collects application-side physical evidence after ARIEC has produced an authoritative
/// hybrid report plan. The tracker records only observations from actual start/slice
/// results. A lack of report traffic is intentionally not converted into signal absence.
/// </summary>
internal sealed class HybridReportPhysicalValidationTracker
{
private sealed class PlanState
{
public required ReportControlPlan Plan { get; init; }
public bool ActivationAttempted { get; set; }
public bool ActivationSucceeded { get; set; }
public string ActivationMessage { get; set; } = "Not attempted";
public string SubscriptionSummary { get; set; } = string.Empty;
public int MemberCount { get; set; }
public int SetupWriteStepCount { get; set; }
public bool UsedDynamicDataSet { get; set; }
public int ReportFrameCount { get; set; }
public int ReportUpdateCount { get; set; }
public HashSet<string> ChangeVerifiedPointKeys { get; } = new(StringComparer.OrdinalIgnoreCase);
public DateTimeOffset? FirstReportAtUtc { get; set; }
public DateTimeOffset? LastReportAtUtc { get; set; }
}

private readonly Dictionary<string, PlanState> _plans = new(StringComparer.OrdinalIgnoreCase);
private readonly List<string> _warnings = new();
private NativeHybridReportPlanningResult? _planning;

public void Reset(NativeHybridReportPlanningResult? planning)
{
_planning = planning;
_plans.Clear();
_warnings.Clear();

if (planning is null)
return;

foreach (var warning in planning.Warnings)
AddWarning(warning);

foreach (var plan in planning.ReportPlans)
_plans[plan.PlanId] = new PlanState { Plan = plan };
}

public void RecordActivation(ReportControlPlan plan, NativeReportMonitorStartResult result)
{
if (!plan.IsEngineAuthoritative)
return;

if (!_plans.TryGetValue(plan.PlanId, out var state))
{
state = new PlanState { Plan = plan };
_plans[plan.PlanId] = state;
}

state.ActivationAttempted = true;
state.ActivationSucceeded = result.IsSuccess;
state.ActivationMessage = result.Message;
state.SubscriptionSummary = result.SubscriptionSummary;
state.MemberCount = result.MemberCount;
state.SetupWriteStepCount = result.WriteStepCount;
state.UsedDynamicDataSet = result.UsedDynamicDataSet;
foreach (var warning in result.Warnings)
AddWarning(warning);
}

public void RecordSlice(
ReportControlPlan plan,
NativeReportMonitorSliceResult slice,
IEnumerable<string>? changeVerifiedPointKeys = null)
{
if (!plan.IsEngineAuthoritative || !_plans.TryGetValue(plan.PlanId, out var state))
return;

state.ReportFrameCount += slice.ReportFrames.Count;
state.ReportUpdateCount += slice.Updates.Count;

var observedTimes = slice.ReportFrames
.Select(frame => frame.ReceivedAt)
.Where(value => value != default)
.OrderBy(value => value)
.ToArray();
if (observedTimes.Length > 0)
{
state.FirstReportAtUtc ??= observedTimes[0];
state.LastReportAtUtc = observedTimes[^1];
}
else if (slice.ReportFrames.Count > 0 || slice.Updates.Count > 0)
{
var now = DateTimeOffset.UtcNow;
state.FirstReportAtUtc ??= now;
state.LastReportAtUtc = now;
}

if (changeVerifiedPointKeys is not null)
{
foreach (var key in changeVerifiedPointKeys.Where(key => !string.IsNullOrWhiteSpace(key)))
state.ChangeVerifiedPointKeys.Add(key);
}

foreach (var warning in slice.Warnings)
AddWarning(warning);
}

public HybridReportPhysicalValidationSnapshot Capture(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);
var planning = _planning;
var plans = _plans.Values
.OrderBy(state => state.Plan.EngineAcquisitionKind, StringComparer.OrdinalIgnoreCase)
Comment on lines +112 to +114

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 Synchronize physical-validation snapshots

If CaptureHybridReportPhysicalValidation is called while monitoring is active, this enumeration can race with Reset, RecordActivation, or RecordSlice on the monitor task. Those methods mutate the same Dictionary, List, HashSet, counters, and timestamps without synchronization, so capture can throw a collection-modified exception or return a torn snapshot. Protect tracker state with a lock or capture immutable copies under synchronization.

Useful? React with 👍 / 👎.

.ThenBy(state => state.Plan.ReportControlReference, StringComparer.OrdinalIgnoreCase)
.Select(state => new HybridReportPhysicalValidationPlan
{
PlanId = state.Plan.PlanId,
AcquisitionKind = state.Plan.EngineAcquisitionKind,
ReportControlReference = state.Plan.ReportControlReference,
DataSetReference = state.Plan.DataSetReference,
PlannedSignalCount = state.Plan.Bindings.Count,
ActivationSucceeded = state.ActivationSucceeded,
ActivationMessage = state.ActivationMessage,
SubscriptionSummary = state.SubscriptionSummary,
MemberCount = state.MemberCount,
SetupWriteStepCount = state.SetupWriteStepCount,
UsedDynamicDataSet = state.UsedDynamicDataSet,
ReportFrameCount = state.ReportFrameCount,
ReportUpdateCount = state.ReportUpdateCount,
ChangeVerifiedPointCount = state.ChangeVerifiedPointKeys.Count,
FirstReportAtUtc = state.FirstReportAtUtc,
LastReportAtUtc = state.LastReportAtUtc
})
.ToArray();

return new HybridReportPhysicalValidationSnapshot
{
CapturedAtUtc = DateTimeOffset.UtcNow,
DeviceId = device.DeviceId,
DeviceName = device.Name,
Endpoint = device.EndpointText,
PlannedStaticBrcbCount = planning?.StaticBrcbSignalCount ?? 0,
PlannedStaticUrcbCount = planning?.StaticUrcbSignalCount ?? 0,
PlannedDynamicBrcbCount = planning?.DynamicBrcbSignalCount ?? 0,
PlannedDynamicUrcbCount = planning?.DynamicUrcbSignalCount ?? 0,
ActivatedReportPlanCount = _plans.Values.Count(state => state.ActivationAttempted && state.ActivationSucceeded),
FailedActivationCount = _plans.Values.Count(state => state.ActivationAttempted && !state.ActivationSucceeded),
ReportFrameCount = plans.Sum(plan => plan.ReportFrameCount),
ReportUpdateCount = plans.Sum(plan => plan.ReportUpdateCount),
ChangeVerifiedPointCount = plans.Sum(plan => plan.ChangeVerifiedPointCount),
PollingFallbackPointCount = planning?.PollingFallbackSignalCount ?? 0,
UncoveredPointCount = planning?.UncoveredSignalCount ?? 0,
Plans = plans,
Warnings = _warnings.ToArray()
};
}

private void AddWarning(string? warning)
{
var text = (warning ?? string.Empty).Trim();
if (text.Length == 0 || _warnings.Contains(text, StringComparer.OrdinalIgnoreCase))
return;
_warnings.Add(text);
}
}
Loading
Loading