Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
36df5ee
Add clean-room SNTP packet codec
masarray Aug 13, 2026
9b257ad
Add smart SNTP station bus route resolver
masarray Aug 13, 2026
01cd056
Add smart reliable SNTP clock service
masarray Aug 13, 2026
9ec1410
Wire SNTP clock sync to connected IED lifecycle
masarray Aug 13, 2026
54d36f0
Add SNTP packet and broadcast regression tests
masarray Aug 13, 2026
2d4cfe7
Document ARSAS SNTP clock sync P0
masarray Aug 13, 2026
e3955f8
Harden SNTP UDP 123 ownership
masarray Aug 13, 2026
9aa0c79
Harden clock sync shutdown lifecycle
masarray Aug 13, 2026
67214dd
Align SNTP wire fields with RFC 4330
masarray Aug 13, 2026
15c9695
Use RFC-normal SNTP broadcast interval
masarray Aug 13, 2026
4bbd25b
Test RFC compliant SNTP reply fields
masarray Aug 13, 2026
f463c24
Align SNTP documentation with RFC behavior
masarray Aug 13, 2026
50365a8
Prefer Windows routing for SNTP NIC selection
masarray Aug 13, 2026
d45cb08
Use independent WPF clock sync lifecycle hook
masarray Aug 13, 2026
99c413d
Expose clock sync lifecycle initializer
masarray Aug 13, 2026
65a5c4f
Initialize SNTP from existing MainWindow lifecycle
masarray Aug 13, 2026
a1feacd
Use SIPROTEC-compatible SNTP stratum
masarray Aug 13, 2026
18c4624
Describe SIPROTEC compatibility stratum accurately
masarray Aug 13, 2026
03505ad
Lock SIPROTEC compatibility stratum in SNTP tests
masarray Aug 13, 2026
d3efa96
Document SIPROTEC stratum compatibility mode
masarray Aug 13, 2026
6f9d230
Fix SNTP health diagnostic compile scope
masarray Aug 13, 2026
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
169 changes: 169 additions & 0 deletions MainWindow.ClockSync.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System.Collections.Specialized;
using System.ComponentModel;
using System.Net;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

public partial class MainWindow
{
private readonly SntpClockService _sntpClockService = new();
private readonly SemaphoreSlim _clockSyncIntegrationGate = new(1, 1);
private readonly HashSet<string> _clockSyncObservedClients = new(StringComparer.OrdinalIgnoreCase);
private string _lastClockSyncStatus = string.Empty;
private bool _clockSyncLifecycleAttached;

private void InitializeClockSyncLifecycle()
{
if (_clockSyncLifecycleAttached)
return;

_clockSyncLifecycleAttached = true;
Devices.CollectionChanged += ClockSyncDevices_CollectionChanged;
foreach (var device in Devices)
AttachClockSyncDevice(device);

_sntpClockService.StatusChanged += ClockSyncService_StatusChanged;
_sntpClockService.ClientRequestObserved += ClockSyncService_ClientRequestObserved;
Closed += ClockSyncMainWindow_Closed;
}

private void ClockSyncDevices_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems.OfType<Iec61850MonitorDevice>())
item.PropertyChanged -= ClockSyncDevice_PropertyChanged;
}

if (e.NewItems != null)
{
foreach (var item in e.NewItems.OfType<Iec61850MonitorDevice>())
AttachClockSyncDevice(item);
}
}

private void AttachClockSyncDevice(Iec61850MonitorDevice device)
{
device.PropertyChanged -= ClockSyncDevice_PropertyChanged;
device.PropertyChanged += ClockSyncDevice_PropertyChanged;

if (device.IsConnected)
ScheduleClockSyncReconcile(device);
Comment on lines +52 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip demo devices before starting SNTP

When demo mode adds its simulated devices, each is already marked IsConnected = true and IsDemo = true before Devices.Add (MainWindow.Demo.cs lines 209-222), so this unconditional scheduling starts a real UDP/123 service for fake addresses such as 192.168.10.11. If that subnet is absent, route resolution can select the machine's default adapter, causing demo mode to bind UDP/123 and transmit recurring time broadcasts onto an unrelated real network; exclude demo devices from clock-sync reconciliation.

Useful? React with 👍 / 👎.

}

private void ClockSyncDevice_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(Iec61850MonitorDevice.IsConnected) ||
sender is not Iec61850MonitorDevice device ||
!device.IsConnected)
return;

ScheduleClockSyncReconcile(device);
}

private void ScheduleClockSyncReconcile(Iec61850MonitorDevice device)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new Action(() => ScheduleClockSyncReconcile(device)));
return;
}

_ = EnsureClockSyncForDeviceAsync(device);
}

private async Task EnsureClockSyncForDeviceAsync(Iec61850MonitorDevice device)
{
if (!IPAddress.TryParse(device.IpAddress, out var iedAddress) ||
iedAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
return;

await _clockSyncIntegrationGate.WaitAsync();
try
{
await _sntpClockService.EnsureStartedAsync(iedAddress, _applicationCancellation.Token);
_sntpClockService.RequestImmediateBroadcast();
}
catch (OperationCanceledException) when (_applicationCancellation.IsCancellationRequested)
{
}
catch (Exception ex)
{
AddLog("WARN", "Clock Sync",
$"{device.Name}: IEC 61850 remains connected, but ARSAS SNTP could not start: {ex.Message}");
}
finally
{
_clockSyncIntegrationGate.Release();
}
}

private void ClockSyncService_StatusChanged(SntpClockServiceSnapshot snapshot)
{
void Publish()
{
var status = $"{snapshot.State}|{snapshot.Detail}";
if (status.Equals(_lastClockSyncStatus, StringComparison.Ordinal))
return;

_lastClockSyncStatus = status;
var level = snapshot.State switch
{
SntpClockServiceState.Serving => "INFO",
SntpClockServiceState.Starting => "INFO",
SntpClockServiceState.Stopped => "INFO",
_ => "WARN"
};
AddLog(level, "Clock Sync", snapshot.Detail);
}

if (Dispatcher.CheckAccess())
Publish();
else
Dispatcher.BeginInvoke(new Action(Publish));
}

private void ClockSyncService_ClientRequestObserved(SntpClientObservation observation)
{
var key = observation.Address.ToString();

void Publish()
{
// A request from the same client can occur indefinitely. Keep the live log quiet:
// first observation proves the client is using ARSAS; counters remain in the service snapshot.
if (!_clockSyncObservedClients.Add(key))
return;

var device = Devices.FirstOrDefault(item =>
item.IpAddress.Equals(key, StringComparison.OrdinalIgnoreCase));
var name = device?.Name ?? key;
AddLog("INFO", "Clock Sync",
$"{name} ({key}) requested SNTPv{observation.Version}; ARSAS returned a Mode 4 reply from the station-bus interface.");
}

if (Dispatcher.CheckAccess())
Publish();
else
Dispatcher.BeginInvoke(new Action(Publish));
}

private async void ClockSyncMainWindow_Closed(object? sender, EventArgs e)
{
try
{
Devices.CollectionChanged -= ClockSyncDevices_CollectionChanged;
foreach (var device in Devices)
device.PropertyChanged -= ClockSyncDevice_PropertyChanged;

_sntpClockService.StatusChanged -= ClockSyncService_StatusChanged;
_sntpClockService.ClientRequestObserved -= ClockSyncService_ClientRequestObserved;
await _sntpClockService.DisposeAsync();
}
catch
{
// Application shutdown must never be blocked by a commissioning helper service.
}
}
}
1 change: 1 addition & 0 deletions MainWindow.IoTesting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public partial class MainWindow
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
InitializeClockSyncLifecycle();
Dispatcher.BeginInvoke(new Action(InstallFirstRunTestingChoices), DispatcherPriority.Loaded);
}

Expand Down
Loading
Loading