From aacd84b7693ff6aa27e633ea331bb29ffc36c2d2 Mon Sep 17 00:00:00 2001 From: masarray Date: Thu, 16 Jul 2026 15:50:51 +0700 Subject: [PATCH 01/13] chore: add one-shot GOOSE lightweight correction --- scripts/apply-goose-lightweight-fix.py | 462 +++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 scripts/apply-goose-lightweight-fix.py diff --git a/scripts/apply-goose-lightweight-fix.py b/scripts/apply-goose-lightweight-fix.py new file mode 100644 index 000000000..deee1b32a --- /dev/null +++ b/scripts/apply-goose-lightweight-fix.py @@ -0,0 +1,462 @@ +from pathlib import Path +import re + + +def replace_once(text: str, old: str, new: str, label: str) -> str: + count = text.count(old) + if count != 1: + raise RuntimeError(f"{label}: expected one match, found {count}") + return text.replace(old, new, 1) + + +# Models/GooseSubscriberModels.cs +path = Path("Models/GooseSubscriberModels.cs") +text = path.read_text(encoding="utf-8") +text = replace_once( + text, + ''' public string MacAddress { get; init; } = string.Empty; + public string Selector => Index.ToString(System.Globalization.CultureInfo.InvariantCulture); + public string DisplayText => $"[{Index}] {(!string.IsNullOrWhiteSpace(Description) ? Description : Name)}"; + public string DetailText => string.IsNullOrWhiteSpace(MacAddress) ? Name : $"{MacAddress} • {Name}";''', + ''' public string MacAddress { get; init; } = string.Empty; + public string FriendlyName { get; init; } = string.Empty; + public string Selector => Index.ToString(System.Globalization.CultureInfo.InvariantCulture); + public string DisplayText => $"[{Index}] {FirstReadable(FriendlyName, Description, Name, "Network adapter")}"; + public string DetailText => string.Join(" • ", new[] { FriendlyName, Description, MacAddress, Name } + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.OrdinalIgnoreCase)); + + private static string FirstReadable(params string?[] values) + => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? "Network adapter";''', + "adapter presentation", +) +text = replace_once( + text, + ''' private string _bindingSource = "Unbound"; + private bool _isChanged;''', + ''' private string _bindingSource = "Unbound"; + private bool _isChanged; + private bool _isHighlighted; + private DateTimeOffset _highlightUntilUtc;''', + "leaf highlight fields", +) +text = replace_once( + text, + ''' public bool IsChanged { get => _isChanged; set => Set(ref _isChanged, value); } + public string TypeText => string.Join(" / ", new[] { Cdc, BType }.Where(item => !string.IsNullOrWhiteSpace(item)));''', + ''' public bool IsChanged { get => _isChanged; set => Set(ref _isChanged, value); } + public bool IsHighlighted { get => _isHighlighted; private set => Set(ref _isHighlighted, value); } + public string TypeText => string.Join(" / ", new[] { Cdc, BType }.Where(item => !string.IsNullOrWhiteSpace(item))); + + public bool ExpireHighlight(DateTimeOffset nowUtc) + { + if (!IsHighlighted || nowUtc < _highlightUntilUtc) + return false; + IsHighlighted = false; + return true; + }''', + "leaf highlight property", +) +text = replace_once( + text, + ''' BindingSource = snapshot.BindingSource; + IsChanged = snapshot.IsChanged; + Raise(nameof(TypeText));''', + ''' BindingSource = snapshot.BindingSource; + IsChanged = snapshot.IsChanged; + if (snapshot.IsChanged) + { + _highlightUntilUtc = DateTimeOffset.UtcNow.AddSeconds(5); + IsHighlighted = true; + } + Raise(nameof(TypeText));''', + "leaf highlight arm", +) +path.write_text(text, encoding="utf-8") + +# Models/GoosePresentationModels.cs +path = Path("Models/GoosePresentationModels.cs") +text = path.read_text(encoding="utf-8") +text = replace_once(text, "public sealed class GooseEventRow\n{", "public sealed class GooseEventRow : ObservableObject\n{", "event observable") +text = replace_once( + text, + ''' public required string Summary { get; init; } + + public string TimeText => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);''', + ''' public required string Summary { get; init; } + + private bool _isRecent = true; + public bool IsRecent { get => _isRecent; private set => Set(ref _isRecent, value); } + public string TimeText => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture); + + public bool ExpireHighlight(DateTimeOffset nowUtc) + { + if (!IsRecent || nowUtc - Timestamp < TimeSpan.FromSeconds(5)) + return false; + IsRecent = false; + return true; + }''', + "event recent expiry", +) +path.write_text(text, encoding="utf-8") + +# MainWindow.GooseTimeline.cs +path = Path("MainWindow.GooseTimeline.cs") +text = path.read_text(encoding="utf-8") +text = replace_once(text, "private const int MaxGooseTimelineEvents = 1000;\n private const int MaxPendingGooseTimelineEvents = 4096;", "private const int MaxGooseTimelineEvents = 300;\n private const int MaxPendingGooseTimelineEvents = 512;", "timeline limits") +text = replace_once( + text, + ''' private GooseEventRow? _selectedGooseEvent; + private bool _goosePresentationInstalled;''', + ''' private GooseEventRow? _selectedGooseEvent; + private bool _goosePresentationInstalled; + private DateTimeOffset _nextGooseHighlightExpiryCheckUtc = DateTimeOffset.MinValue;''', + "timeline expiry field", +) +old = ''' private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args) + { + if (_pendingGooseTimeline.IsEmpty) + { + Raise(nameof(GoosePublisherCountText)); + Raise(nameof(GooseSelectedLeafCountText)); + return; + } + + var processed = 0; + while (processed < 256 && _pendingGooseTimeline.TryDequeue(out var captured))''' +new = ''' private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args) + { + var nowUtc = DateTimeOffset.UtcNow; + if (nowUtc >= _nextGooseHighlightExpiryCheckUtc) + { + ExpireGooseHighlights(nowUtc); + _nextGooseHighlightExpiryCheckUtc = nowUtc.AddSeconds(1); + } + + if (_pendingGooseTimeline.IsEmpty) + return; + + var processed = 0; + while (processed < 48 && _pendingGooseTimeline.TryDequeue(out var captured))''' +text = replace_once(text, old, new, "timeline flush") +insert_anchor = ''' private GooseEventRow BuildGooseEventRow( + GooseSubscriberFrameSnapshot captured, + GooseStreamSnapshot stream)''' +insert_code = ''' private void ExpireGooseHighlights(DateTimeOffset nowUtc) + { + foreach (var eventRow in GooseEvents) + eventRow.ExpireHighlight(nowUtc); + + foreach (var stream in GooseStreams) + { + foreach (var leaf in stream.Leaves) + leaf.ExpireHighlight(nowUtc); + } + } + +''' + insert_anchor +text = replace_once(text, insert_anchor, insert_code, "highlight expiry method") +text = replace_once( + text, + ''' _lastGooseTimelineTimestamp.Clear(); + GooseEvents.Clear();''', + ''' _lastGooseTimelineTimestamp.Clear(); + _nextGooseHighlightExpiryCheckUtc = DateTimeOffset.MinValue; + GooseEvents.Clear();''', + "reset expiry", +) +path.write_text(text, encoding="utf-8") + +# MainWindow.GooseSubscriber.cs: semantic values and fixed internal filter +path = Path("MainWindow.GooseSubscriber.cs") +text = path.read_text(encoding="utf-8") +text = replace_once(text, ' private string _gooseCaptureFilter = GooseSubscriberRuntime.DefaultCaptureFilter;\n', '', "remove editable filter field") +text = replace_once(text, ' public string GooseCaptureFilter { get => _gooseCaptureFilter; set => Set(ref _gooseCaptureFilter, value ?? string.Empty); }\n', '', "remove editable filter property") +text = replace_once(text, ' GooseCaptureFilter,\n', ' GooseSubscriberRuntime.DefaultCaptureFilter,\n', "fixed capture filter") +old_value = ''' var value = index < rawValueCount + ? decoded?.DisplayValue ?? MmsDataValueRenderer.ToCompactString(frame.Pdu.Values[index], signalReference) + : ""; + + leaves.Add(new GooseLeafValueSnapshot(''' +new_value = ''' var rawValue = index < rawValueCount + ? decoded?.DisplayValue ?? MmsDataValueRenderer.ToCompactString(frame.Pdu.Values[index], signalReference) + : ""; + var value = InterpretGooseLeafValue(rawValue, definition?.Cdc, definition?.BType); + var previousValue = InterpretGooseLeafValue(decoded?.PreviousDisplayValue ?? string.Empty, definition?.Cdc, definition?.BType); + + leaves.Add(new GooseLeafValueSnapshot(''' +text = replace_once(text, old_value, new_value, "semantic value decode") +text = replace_once(text, ' decoded?.PreviousDisplayValue ?? string.Empty,\n', ' previousValue,\n', "semantic previous value") +anchor = ''' private static string FirstNonEmpty(params string?[] values) + => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;''' +helper = ''' private static string InterpretGooseLeafValue(string? rawValue, string? cdc, string? bType) + { + var text = rawValue?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(text) || text == "") + return text; + + var bitString = Regex.Match(text, @"^bits\((?[0-9A-Fa-f]{2}),\s*unused=(?[67])\)$", RegexOptions.CultureInvariant); + if (!bitString.Success || !byte.TryParse(bitString.Groups["hex"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + return text; + + var unused = bitString.Groups["unused"].Value; + if (unused == "7") + return (value & 0x80) == 0 ? "false" : "true"; + + var doublePoint = (value >> 6) & 0x03; + return doublePoint switch + { + 0 => "Intermediate [00]", + 1 => "Open [01]", + 2 => "Closed [10]", + _ => "Invalid [11]" + }; + } + +''' + anchor +text = replace_once(text, anchor, helper, "semantic interpreter helper") +path.write_text(text, encoding="utf-8") + +# Services/GooseSubscriberRuntime.cs: real Windows adapter name +path = Path("Services/GooseSubscriberRuntime.cs") +text = path.read_text(encoding="utf-8") +text = replace_once(text, "using System.Diagnostics;\n", "using System.Diagnostics;\nusing System.Net.NetworkInformation;\nusing System.Text.RegularExpressions;\n", "network imports") +old_list = ''' public IReadOnlyList ListAdapters() + => NpcapAdapterCatalog.ListAdapters() + .Select(adapter => new GooseAdapterOption + { + Index = adapter.Index, + Name = adapter.Name, + Description = adapter.Description, + MacAddress = adapter.MacAddress?.ToString() ?? string.Empty + }) + .ToArray();''' +new_list = ''' public IReadOnlyList ListAdapters() + { + var windowsAdapters = NetworkInterface.GetAllNetworkInterfaces(); + return NpcapAdapterCatalog.ListAdapters() + .Select(adapter => + { + var macAddress = adapter.MacAddress?.ToString() ?? string.Empty; + return new GooseAdapterOption + { + Index = adapter.Index, + Name = adapter.Name, + Description = adapter.Description, + MacAddress = macAddress, + FriendlyName = ResolveAdapterFriendlyName(adapter.Name, adapter.Description, macAddress, windowsAdapters) + }; + }) + .ToArray(); + } + + private static string ResolveAdapterFriendlyName( + string captureName, + string captureDescription, + string macAddress, + IReadOnlyList windowsAdapters) + { + var normalizedMac = NormalizeMac(macAddress); + var captureId = ExtractAdapterId(captureName); + var match = windowsAdapters.FirstOrDefault(adapter => + (!string.IsNullOrWhiteSpace(normalizedMac) && NormalizeMac(adapter.GetPhysicalAddress().ToString()) == normalizedMac) || + (!string.IsNullOrWhiteSpace(captureId) && adapter.Id.Equals(captureId, StringComparison.OrdinalIgnoreCase))); + + if (match is not null) + { + var windowsName = CleanAdapterLabel(match.Name); + if (!string.IsNullOrWhiteSpace(windowsName)) + return windowsName; + var windowsDescription = CleanAdapterLabel(match.Description); + if (!string.IsNullOrWhiteSpace(windowsDescription)) + return windowsDescription; + } + + return FirstAdapterLabel(CleanAdapterLabel(captureDescription), CleanAdapterLabel(captureName), "Network adapter"); + } + + private static string ExtractAdapterId(string? value) + { + var match = Regex.Match(value ?? string.Empty, @"\{(?[0-9A-Fa-f-]{36})\}"); + return match.Success ? match.Groups["id"].Value : string.Empty; + } + + private static string NormalizeMac(string? value) + => Regex.Replace(value ?? string.Empty, "[^0-9A-Fa-f]", string.Empty).ToUpperInvariant(); + + private static string CleanAdapterLabel(string? value) + { + var text = value?.Trim() ?? string.Empty; + if (text.Equals("ArIED61850", StringComparison.OrdinalIgnoreCase) || + text.Equals("ArIED 61850", StringComparison.OrdinalIgnoreCase)) + return string.Empty; + return text; + } + + private static string FirstAdapterLabel(params string[] values) + => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? "Network adapter";''' +text = replace_once(text, old_list, new_list, "adapter friendly mapping") +path.write_text(text, encoding="utf-8") + +# Views/GooseSubscriberView.xaml: remove advanced filter and use 5-second transient highlights +path = Path("Views/GooseSubscriberView.xaml") +text = path.read_text(encoding="utf-8") +text = replace_once( + text, + ''' + + + + + + + + + + + + ''', + ''' + + + + + + + + + + ''', + "capture bar columns", +) +old_controls = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 25020083dece46264de0ce77641ae4f7ce729976 Mon Sep 17 00:00:00 2001 From: masarray Date: Thu, 16 Jul 2026 15:58:37 +0700 Subject: [PATCH 11/13] fix: expire highlights and throttle GOOSE timeline UI --- MainWindow.GooseTimeline.cs | 49 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/MainWindow.GooseTimeline.cs b/MainWindow.GooseTimeline.cs index 7fb81b238..ef9770c30 100644 --- a/MainWindow.GooseTimeline.cs +++ b/MainWindow.GooseTimeline.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; -using System.Windows.Threading; using ArIED61850Tester.Models; using ArIED61850Tester.Services; using ArIED61850Tester.Views; @@ -13,13 +12,14 @@ namespace ArIED61850Tester; public partial class MainWindow { - private const int MaxGooseTimelineEvents = 1000; - private const int MaxPendingGooseTimelineEvents = 4096; + private const int MaxGooseTimelineEvents = 300; + private const int MaxPendingGooseTimelineEvents = 512; private readonly ConcurrentQueue _pendingGooseTimeline = new(); private readonly Dictionary _lastGooseTimelineTimestamp = new(StringComparer.OrdinalIgnoreCase); private GooseEventRow? _selectedGooseEvent; private bool _goosePresentationInstalled; + private DateTimeOffset _nextGooseHighlightExpiryCheckUtc = DateTimeOffset.MinValue; public BulkObservableCollection GooseEvents { get; } = new(); @@ -50,23 +50,23 @@ internal static void RegisterGoosePresentationIntegration() new RoutedEventHandler(OnMainWindowLoadedForGoosePresentation)); EventManager.RegisterClassHandler( typeof(MainWindow), - GooseSubscriberView.RefreshAdaptersRequestedEvent, + GooseSubscriberLiteView.RefreshAdaptersRequestedEvent, new RoutedEventHandler(OnRefreshGooseAdaptersRequested)); EventManager.RegisterClassHandler( typeof(MainWindow), - GooseSubscriberView.RefreshModelsRequestedEvent, + GooseSubscriberLiteView.RefreshModelsRequestedEvent, new RoutedEventHandler(OnRefreshGooseModelsRequested)); EventManager.RegisterClassHandler( typeof(MainWindow), - GooseSubscriberView.StartRequestedEvent, + GooseSubscriberLiteView.StartRequestedEvent, new RoutedEventHandler(OnStartGooseRequested)); EventManager.RegisterClassHandler( typeof(MainWindow), - GooseSubscriberView.StopRequestedEvent, + GooseSubscriberLiteView.StopRequestedEvent, new RoutedEventHandler(OnStopGooseRequested)); EventManager.RegisterClassHandler( typeof(MainWindow), - GooseSubscriberView.ClearRequestedEvent, + GooseSubscriberLiteView.ClearRequestedEvent, new RoutedEventHandler(OnClearGooseRequested)); } @@ -87,7 +87,7 @@ private void InstallGoosePresentationWorkspace() if (gooseTab is null) return; - gooseTab.Content = new GooseSubscriberView { DataContext = this }; + gooseTab.Content = new GooseSubscriberLiteView { DataContext = this }; _gooseSubscriberRuntime.FrameReceived += GooseTimeline_FrameReceived; _uiFlushTimer.Tick += GooseTimelineUiFlushTimer_Tick; PropertyChanged += GoosePresentation_PropertyChanged; @@ -167,15 +167,18 @@ private static bool IsMeaningfulGooseTimelineEvent(GooseSubscriberFrameSnapshot private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args) { - if (_pendingGooseTimeline.IsEmpty) + var nowUtc = DateTimeOffset.UtcNow; + if (nowUtc >= _nextGooseHighlightExpiryCheckUtc) { - Raise(nameof(GoosePublisherCountText)); - Raise(nameof(GooseSelectedLeafCountText)); - return; + ExpireGooseHighlights(nowUtc); + _nextGooseHighlightExpiryCheckUtc = nowUtc.AddSeconds(1); } + if (_pendingGooseTimeline.IsEmpty) + return; + var processed = 0; - while (processed < 256 && _pendingGooseTimeline.TryDequeue(out var captured)) + while (processed < 48 && _pendingGooseTimeline.TryDequeue(out var captured)) { var stream = BuildGooseStreamSnapshot(captured, _gooseBindingCatalog); var eventRow = BuildGooseEventRow(captured, stream); @@ -190,6 +193,18 @@ private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args) RaiseGoosePresentationState(); } + private void ExpireGooseHighlights(DateTimeOffset nowUtc) + { + foreach (var eventRow in GooseEvents) + eventRow.ExpireHighlight(nowUtc); + + foreach (var stream in GooseStreams) + { + foreach (var leaf in stream.Leaves) + leaf.ExpireHighlight(nowUtc); + } + } + private GooseEventRow BuildGooseEventRow( GooseSubscriberFrameSnapshot captured, GooseStreamSnapshot stream) @@ -256,8 +271,9 @@ private static string BuildGooseEventSummary(GooseStreamSnapshot stream, bool is { var previous = string.IsNullOrWhiteSpace(leaf.PreviousValue) ? "-" - : ShortenGooseText(leaf.PreviousValue, 30); - return $"{leaf.SignalName}: {previous} → {ShortenGooseText(leaf.Value, 36)}"; + : ShortenGooseText(GooseEngineeringValueFormatter.Format(leaf.PreviousValue), 30); + var current = GooseEngineeringValueFormatter.Format(leaf.Value); + return $"{leaf.SignalName}: {previous} → {ShortenGooseText(current, 36)}"; }) .ToArray(); if (changed.Length > 0) @@ -322,6 +338,7 @@ private void ResetGooseTimelineUi() { } _lastGooseTimelineTimestamp.Clear(); + _nextGooseHighlightExpiryCheckUtc = DateTimeOffset.MinValue; GooseEvents.Clear(); SelectedGooseEvent = null; RaiseGoosePresentationState(); From 22af6abdb812813200ac18b9070f4afb95cdd842 Mon Sep 17 00:00:00 2001 From: masarray Date: Thu, 16 Jul 2026 15:59:07 +0700 Subject: [PATCH 12/13] chore: remove one-shot GOOSE patch helper --- scripts/apply-goose-lightweight-fix.py | 462 ------------------------- 1 file changed, 462 deletions(-) delete mode 100644 scripts/apply-goose-lightweight-fix.py diff --git a/scripts/apply-goose-lightweight-fix.py b/scripts/apply-goose-lightweight-fix.py deleted file mode 100644 index deee1b32a..000000000 --- a/scripts/apply-goose-lightweight-fix.py +++ /dev/null @@ -1,462 +0,0 @@ -from pathlib import Path -import re - - -def replace_once(text: str, old: str, new: str, label: str) -> str: - count = text.count(old) - if count != 1: - raise RuntimeError(f"{label}: expected one match, found {count}") - return text.replace(old, new, 1) - - -# Models/GooseSubscriberModels.cs -path = Path("Models/GooseSubscriberModels.cs") -text = path.read_text(encoding="utf-8") -text = replace_once( - text, - ''' public string MacAddress { get; init; } = string.Empty; - public string Selector => Index.ToString(System.Globalization.CultureInfo.InvariantCulture); - public string DisplayText => $"[{Index}] {(!string.IsNullOrWhiteSpace(Description) ? Description : Name)}"; - public string DetailText => string.IsNullOrWhiteSpace(MacAddress) ? Name : $"{MacAddress} • {Name}";''', - ''' public string MacAddress { get; init; } = string.Empty; - public string FriendlyName { get; init; } = string.Empty; - public string Selector => Index.ToString(System.Globalization.CultureInfo.InvariantCulture); - public string DisplayText => $"[{Index}] {FirstReadable(FriendlyName, Description, Name, "Network adapter")}"; - public string DetailText => string.Join(" • ", new[] { FriendlyName, Description, MacAddress, Name } - .Where(value => !string.IsNullOrWhiteSpace(value)) - .Distinct(StringComparer.OrdinalIgnoreCase)); - - private static string FirstReadable(params string?[] values) - => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? "Network adapter";''', - "adapter presentation", -) -text = replace_once( - text, - ''' private string _bindingSource = "Unbound"; - private bool _isChanged;''', - ''' private string _bindingSource = "Unbound"; - private bool _isChanged; - private bool _isHighlighted; - private DateTimeOffset _highlightUntilUtc;''', - "leaf highlight fields", -) -text = replace_once( - text, - ''' public bool IsChanged { get => _isChanged; set => Set(ref _isChanged, value); } - public string TypeText => string.Join(" / ", new[] { Cdc, BType }.Where(item => !string.IsNullOrWhiteSpace(item)));''', - ''' public bool IsChanged { get => _isChanged; set => Set(ref _isChanged, value); } - public bool IsHighlighted { get => _isHighlighted; private set => Set(ref _isHighlighted, value); } - public string TypeText => string.Join(" / ", new[] { Cdc, BType }.Where(item => !string.IsNullOrWhiteSpace(item))); - - public bool ExpireHighlight(DateTimeOffset nowUtc) - { - if (!IsHighlighted || nowUtc < _highlightUntilUtc) - return false; - IsHighlighted = false; - return true; - }''', - "leaf highlight property", -) -text = replace_once( - text, - ''' BindingSource = snapshot.BindingSource; - IsChanged = snapshot.IsChanged; - Raise(nameof(TypeText));''', - ''' BindingSource = snapshot.BindingSource; - IsChanged = snapshot.IsChanged; - if (snapshot.IsChanged) - { - _highlightUntilUtc = DateTimeOffset.UtcNow.AddSeconds(5); - IsHighlighted = true; - } - Raise(nameof(TypeText));''', - "leaf highlight arm", -) -path.write_text(text, encoding="utf-8") - -# Models/GoosePresentationModels.cs -path = Path("Models/GoosePresentationModels.cs") -text = path.read_text(encoding="utf-8") -text = replace_once(text, "public sealed class GooseEventRow\n{", "public sealed class GooseEventRow : ObservableObject\n{", "event observable") -text = replace_once( - text, - ''' public required string Summary { get; init; } - - public string TimeText => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);''', - ''' public required string Summary { get; init; } - - private bool _isRecent = true; - public bool IsRecent { get => _isRecent; private set => Set(ref _isRecent, value); } - public string TimeText => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture); - - public bool ExpireHighlight(DateTimeOffset nowUtc) - { - if (!IsRecent || nowUtc - Timestamp < TimeSpan.FromSeconds(5)) - return false; - IsRecent = false; - return true; - }''', - "event recent expiry", -) -path.write_text(text, encoding="utf-8") - -# MainWindow.GooseTimeline.cs -path = Path("MainWindow.GooseTimeline.cs") -text = path.read_text(encoding="utf-8") -text = replace_once(text, "private const int MaxGooseTimelineEvents = 1000;\n private const int MaxPendingGooseTimelineEvents = 4096;", "private const int MaxGooseTimelineEvents = 300;\n private const int MaxPendingGooseTimelineEvents = 512;", "timeline limits") -text = replace_once( - text, - ''' private GooseEventRow? _selectedGooseEvent; - private bool _goosePresentationInstalled;''', - ''' private GooseEventRow? _selectedGooseEvent; - private bool _goosePresentationInstalled; - private DateTimeOffset _nextGooseHighlightExpiryCheckUtc = DateTimeOffset.MinValue;''', - "timeline expiry field", -) -old = ''' private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args) - { - if (_pendingGooseTimeline.IsEmpty) - { - Raise(nameof(GoosePublisherCountText)); - Raise(nameof(GooseSelectedLeafCountText)); - return; - } - - var processed = 0; - while (processed < 256 && _pendingGooseTimeline.TryDequeue(out var captured))''' -new = ''' private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args) - { - var nowUtc = DateTimeOffset.UtcNow; - if (nowUtc >= _nextGooseHighlightExpiryCheckUtc) - { - ExpireGooseHighlights(nowUtc); - _nextGooseHighlightExpiryCheckUtc = nowUtc.AddSeconds(1); - } - - if (_pendingGooseTimeline.IsEmpty) - return; - - var processed = 0; - while (processed < 48 && _pendingGooseTimeline.TryDequeue(out var captured))''' -text = replace_once(text, old, new, "timeline flush") -insert_anchor = ''' private GooseEventRow BuildGooseEventRow( - GooseSubscriberFrameSnapshot captured, - GooseStreamSnapshot stream)''' -insert_code = ''' private void ExpireGooseHighlights(DateTimeOffset nowUtc) - { - foreach (var eventRow in GooseEvents) - eventRow.ExpireHighlight(nowUtc); - - foreach (var stream in GooseStreams) - { - foreach (var leaf in stream.Leaves) - leaf.ExpireHighlight(nowUtc); - } - } - -''' + insert_anchor -text = replace_once(text, insert_anchor, insert_code, "highlight expiry method") -text = replace_once( - text, - ''' _lastGooseTimelineTimestamp.Clear(); - GooseEvents.Clear();''', - ''' _lastGooseTimelineTimestamp.Clear(); - _nextGooseHighlightExpiryCheckUtc = DateTimeOffset.MinValue; - GooseEvents.Clear();''', - "reset expiry", -) -path.write_text(text, encoding="utf-8") - -# MainWindow.GooseSubscriber.cs: semantic values and fixed internal filter -path = Path("MainWindow.GooseSubscriber.cs") -text = path.read_text(encoding="utf-8") -text = replace_once(text, ' private string _gooseCaptureFilter = GooseSubscriberRuntime.DefaultCaptureFilter;\n', '', "remove editable filter field") -text = replace_once(text, ' public string GooseCaptureFilter { get => _gooseCaptureFilter; set => Set(ref _gooseCaptureFilter, value ?? string.Empty); }\n', '', "remove editable filter property") -text = replace_once(text, ' GooseCaptureFilter,\n', ' GooseSubscriberRuntime.DefaultCaptureFilter,\n', "fixed capture filter") -old_value = ''' var value = index < rawValueCount - ? decoded?.DisplayValue ?? MmsDataValueRenderer.ToCompactString(frame.Pdu.Values[index], signalReference) - : ""; - - leaves.Add(new GooseLeafValueSnapshot(''' -new_value = ''' var rawValue = index < rawValueCount - ? decoded?.DisplayValue ?? MmsDataValueRenderer.ToCompactString(frame.Pdu.Values[index], signalReference) - : ""; - var value = InterpretGooseLeafValue(rawValue, definition?.Cdc, definition?.BType); - var previousValue = InterpretGooseLeafValue(decoded?.PreviousDisplayValue ?? string.Empty, definition?.Cdc, definition?.BType); - - leaves.Add(new GooseLeafValueSnapshot(''' -text = replace_once(text, old_value, new_value, "semantic value decode") -text = replace_once(text, ' decoded?.PreviousDisplayValue ?? string.Empty,\n', ' previousValue,\n', "semantic previous value") -anchor = ''' private static string FirstNonEmpty(params string?[] values) - => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;''' -helper = ''' private static string InterpretGooseLeafValue(string? rawValue, string? cdc, string? bType) - { - var text = rawValue?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(text) || text == "") - return text; - - var bitString = Regex.Match(text, @"^bits\((?[0-9A-Fa-f]{2}),\s*unused=(?[67])\)$", RegexOptions.CultureInvariant); - if (!bitString.Success || !byte.TryParse(bitString.Groups["hex"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) - return text; - - var unused = bitString.Groups["unused"].Value; - if (unused == "7") - return (value & 0x80) == 0 ? "false" : "true"; - - var doublePoint = (value >> 6) & 0x03; - return doublePoint switch - { - 0 => "Intermediate [00]", - 1 => "Open [01]", - 2 => "Closed [10]", - _ => "Invalid [11]" - }; - } - -''' + anchor -text = replace_once(text, anchor, helper, "semantic interpreter helper") -path.write_text(text, encoding="utf-8") - -# Services/GooseSubscriberRuntime.cs: real Windows adapter name -path = Path("Services/GooseSubscriberRuntime.cs") -text = path.read_text(encoding="utf-8") -text = replace_once(text, "using System.Diagnostics;\n", "using System.Diagnostics;\nusing System.Net.NetworkInformation;\nusing System.Text.RegularExpressions;\n", "network imports") -old_list = ''' public IReadOnlyList ListAdapters() - => NpcapAdapterCatalog.ListAdapters() - .Select(adapter => new GooseAdapterOption - { - Index = adapter.Index, - Name = adapter.Name, - Description = adapter.Description, - MacAddress = adapter.MacAddress?.ToString() ?? string.Empty - }) - .ToArray();''' -new_list = ''' public IReadOnlyList ListAdapters() - { - var windowsAdapters = NetworkInterface.GetAllNetworkInterfaces(); - return NpcapAdapterCatalog.ListAdapters() - .Select(adapter => - { - var macAddress = adapter.MacAddress?.ToString() ?? string.Empty; - return new GooseAdapterOption - { - Index = adapter.Index, - Name = adapter.Name, - Description = adapter.Description, - MacAddress = macAddress, - FriendlyName = ResolveAdapterFriendlyName(adapter.Name, adapter.Description, macAddress, windowsAdapters) - }; - }) - .ToArray(); - } - - private static string ResolveAdapterFriendlyName( - string captureName, - string captureDescription, - string macAddress, - IReadOnlyList windowsAdapters) - { - var normalizedMac = NormalizeMac(macAddress); - var captureId = ExtractAdapterId(captureName); - var match = windowsAdapters.FirstOrDefault(adapter => - (!string.IsNullOrWhiteSpace(normalizedMac) && NormalizeMac(adapter.GetPhysicalAddress().ToString()) == normalizedMac) || - (!string.IsNullOrWhiteSpace(captureId) && adapter.Id.Equals(captureId, StringComparison.OrdinalIgnoreCase))); - - if (match is not null) - { - var windowsName = CleanAdapterLabel(match.Name); - if (!string.IsNullOrWhiteSpace(windowsName)) - return windowsName; - var windowsDescription = CleanAdapterLabel(match.Description); - if (!string.IsNullOrWhiteSpace(windowsDescription)) - return windowsDescription; - } - - return FirstAdapterLabel(CleanAdapterLabel(captureDescription), CleanAdapterLabel(captureName), "Network adapter"); - } - - private static string ExtractAdapterId(string? value) - { - var match = Regex.Match(value ?? string.Empty, @"\{(?[0-9A-Fa-f-]{36})\}"); - return match.Success ? match.Groups["id"].Value : string.Empty; - } - - private static string NormalizeMac(string? value) - => Regex.Replace(value ?? string.Empty, "[^0-9A-Fa-f]", string.Empty).ToUpperInvariant(); - - private static string CleanAdapterLabel(string? value) - { - var text = value?.Trim() ?? string.Empty; - if (text.Equals("ArIED61850", StringComparison.OrdinalIgnoreCase) || - text.Equals("ArIED 61850", StringComparison.OrdinalIgnoreCase)) - return string.Empty; - return text; - } - - private static string FirstAdapterLabel(params string[] values) - => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? "Network adapter";''' -text = replace_once(text, old_list, new_list, "adapter friendly mapping") -path.write_text(text, encoding="utf-8") - -# Views/GooseSubscriberView.xaml: remove advanced filter and use 5-second transient highlights -path = Path("Views/GooseSubscriberView.xaml") -text = path.read_text(encoding="utf-8") -text = replace_once( - text, - ''' - - - - - - - - - - - - ''', - ''' - - - - - - - - - - ''', - "capture bar columns", -) -old_controls = ''' - - - - - - - - - -