-
Notifications
You must be signed in to change notification settings - Fork 3
Add clean-room SNTP clock sync service #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 9b257ad
Add smart SNTP station bus route resolver
masarray 01cd056
Add smart reliable SNTP clock service
masarray 9ec1410
Wire SNTP clock sync to connected IED lifecycle
masarray 54d36f0
Add SNTP packet and broadcast regression tests
masarray 2d4cfe7
Document ARSAS SNTP clock sync P0
masarray e3955f8
Harden SNTP UDP 123 ownership
masarray 9aa0c79
Harden clock sync shutdown lifecycle
masarray 67214dd
Align SNTP wire fields with RFC 4330
masarray 15c9695
Use RFC-normal SNTP broadcast interval
masarray 4bbd25b
Test RFC compliant SNTP reply fields
masarray f463c24
Align SNTP documentation with RFC behavior
masarray 50365a8
Prefer Windows routing for SNTP NIC selection
masarray d45cb08
Use independent WPF clock sync lifecycle hook
masarray 99c413d
Expose clock sync lifecycle initializer
masarray 65a5c4f
Initialize SNTP from existing MainWindow lifecycle
masarray a1feacd
Use SIPROTEC-compatible SNTP stratum
masarray 18c4624
Describe SIPROTEC compatibility stratum accurately
masarray 03505ad
Lock SIPROTEC compatibility stratum in SNTP tests
masarray d3efa96
Document SIPROTEC stratum compatibility mode
masarray 6f9d230
Fix SNTP health diagnostic compile scope
masarray File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
|
|
||
| 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. | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When demo mode adds its simulated devices, each is already marked
IsConnected = trueandIsDemo = truebeforeDevices.Add(MainWindow.Demo.cslines 209-222), so this unconditional scheduling starts a real UDP/123 service for fake addresses such as192.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 👍 / 👎.