From f1a7cce8de67e1f0a03e099294e4cf76c8b636c1 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:26:39 +0200 Subject: [PATCH 01/25] feat: Replace separate connect/disconnect buttons with single, context-aware one --- .../ViewModels/MainWindowViewModel.cs | 16 ++++++++++++++-- PostCodeSerialMonitor/Views/MainWindow.axaml | 3 +-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 0228d03..8316dc4 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -44,11 +44,18 @@ public partial class MainWindowViewModel : ViewModelBase private ConsoleType selectedConsoleModel; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanToggleConnection))] private string? selectedPort; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanToggleConnection))] + [NotifyPropertyChangedFor(nameof(ConnectionButtonText))] private bool isConnected; + public bool CanToggleConnection => IsConnected || SelectedPort != null; + + public string ConnectionButtonText => IsConnected ? Assets.Resources.Disconnect : Assets.Resources.Connect; + [ObservableProperty] private int selectedTabIndex; @@ -268,8 +275,14 @@ private void RefreshPorts() } [RelayCommand] - private async Task ConnectAsync() + private async Task ToggleConnectionAsync() { + if (IsConnected) + { + Disconnect(); + return; + } + if (SelectedPort != null) { try @@ -303,7 +316,6 @@ await MessageBoxManager } } - [RelayCommand] private void Disconnect() { _serialService.Disconnect(); diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml index 38d3c68..fc82f9b 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml @@ -32,8 +32,7 @@ SelectedItem="{Binding SelectedPort, Mode=TwoWay}" IsEnabled="{Binding IsConnected, Converter={StaticResource BooleanNegationConverter}}"/> From e44eaf9edad44066d69d794ae97ec709ca9739ea Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:58:51 +0200 Subject: [PATCH 03/25] feat: Scale window width better, add 'consider donating' link in footer --- .../Assets/Resources.Designer.cs | 15 +++++++-- .../Assets/Resources.pt-BR.resx | 4 +++ PostCodeSerialMonitor/Assets/Resources.resx | 4 +++ PostCodeSerialMonitor/Views/MainWindow.axaml | 33 +++++++++++++------ 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/PostCodeSerialMonitor/Assets/Resources.Designer.cs b/PostCodeSerialMonitor/Assets/Resources.Designer.cs index 9e55733..49b4789 100644 --- a/PostCodeSerialMonitor/Assets/Resources.Designer.cs +++ b/PostCodeSerialMonitor/Assets/Resources.Designer.cs @@ -641,11 +641,22 @@ public static string AppVersion { /// /// In Views/MainWindow.axaml /// - public static string VisitXboxResearch { - get { + public static string VisitXboxResearch + { + get + { return ResourceManager.GetString("VisitXboxResearch", resourceCulture); } } + + /// + /// In Views/MainWindow.axaml + /// + public static string ConsiderDonation { + get { + return ResourceManager.GetString("ConsiderDonation", resourceCulture); + } + } /// /// In Views/MainWindow.axaml.cs diff --git a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx index 4c8ecd7..21e5c29 100644 --- a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx +++ b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx @@ -377,6 +377,10 @@ Visite XboxResearch.com In Views/MainWindow.axaml + + Se quiser, considere fazer uma doação + In Views/MainWindow.axaml + Pico Firmware: In Views/MainWindow.axaml diff --git a/PostCodeSerialMonitor/Assets/Resources.resx b/PostCodeSerialMonitor/Assets/Resources.resx index 8c40caf..f453ff3 100644 --- a/PostCodeSerialMonitor/Assets/Resources.resx +++ b/PostCodeSerialMonitor/Assets/Resources.resx @@ -377,6 +377,10 @@ Visit XboxResearch.com In Views/MainWindow.axaml + + If you like, consider donating + In Views/MainWindow.axaml + Pico Firmware: In Views/MainWindow.axaml diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml index 3020ccd..842ce77 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml @@ -6,12 +6,12 @@ xmlns:assets="clr-namespace:PostCodeSerialMonitor.Assets" xmlns:converters="clr-namespace:PostCodeSerialMonitor.Views.Converters" xmlns:models="using:PostCodeSerialMonitor.Models" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="900" x:Class="PostCodeSerialMonitor.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Icon="/Assets/avalonia-logo.ico" Title="{x:Static assets:Resources.XboxPostCodeSerialMonitor}" - SizeToContent="Width"> + Width="850" Height="900"> @@ -78,18 +78,31 @@ - + + + + + + + + - + From 456fbb5ef9f388d5a89eebe779e91da4595cbe5f Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:49:14 +0200 Subject: [PATCH 04/25] fix: make hyperlinks in messagebox clickable, unionize hyperlink handling --- .../Assets/Resources.pt-BR.resx | 4 +- PostCodeSerialMonitor/Assets/Resources.resx | 4 +- PostCodeSerialMonitor/Utils/GlobalActions.cs | 21 +++++++++++ .../ViewModels/MainWindowViewModel.cs | 37 ++++++++++++++++--- .../Views/MainWindow.axaml.cs | 7 +--- 5 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 PostCodeSerialMonitor/Utils/GlobalActions.cs diff --git a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx index 21e5c29..084f2a6 100644 --- a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx +++ b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx @@ -234,11 +234,11 @@ In ViewModels/MainWindowViewModel.cs - Uma nova versão do aplicativo está disponível em {0}. + Uma nova versão do aplicativo está disponível In ViewModels/MainWindowViewModel.cs - Uma nova versão do firmware está disponível em {0}. + Uma nova versão do firmware está disponível In ViewModels/MainWindowViewModel.cs diff --git a/PostCodeSerialMonitor/Assets/Resources.resx b/PostCodeSerialMonitor/Assets/Resources.resx index f453ff3..a562bcd 100644 --- a/PostCodeSerialMonitor/Assets/Resources.resx +++ b/PostCodeSerialMonitor/Assets/Resources.resx @@ -234,11 +234,11 @@ In ViewModels/MainWindowViewModel.cs - A new app release is available at {0}. + A new app release is available In ViewModels/MainWindowViewModel.cs - A new firmware release is available at {0}. + A new firmware release is available In ViewModels/MainWindowViewModel.cs diff --git a/PostCodeSerialMonitor/Utils/GlobalActions.cs b/PostCodeSerialMonitor/Utils/GlobalActions.cs new file mode 100644 index 0000000..76db625 --- /dev/null +++ b/PostCodeSerialMonitor/Utils/GlobalActions.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace PostCodeSerialMonitor.Utils; + +public static class GlobalActions +{ + + public static void OpenHyperlinkAction(string url) + { + using var proc = new Process + { + StartInfo = { + UseShellExecute = true, + FileName = url + } + }; + proc.Start(); + } +} \ No newline at end of file diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 88e9301..be7e798 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -14,9 +14,11 @@ using PostCodeSerialMonitor.Views; using PostCodeSerialMonitor.Services; using PostCodeSerialMonitor.Models; +using PostCodeSerialMonitor.Utils; using MsBox.Avalonia; using MsBox.Avalonia.Enums; +using MsBox.Avalonia.Dto; namespace PostCodeSerialMonitor.ViewModels; @@ -127,6 +129,25 @@ public MainWindowViewModel( _serialService.DeviceConfigChanged += OnDeviceConfigChanged; } + private MessageBoxStandardParams MsgBoxHyperlink(string title, string text, string link) + { + return new MessageBoxStandardParams + { + ContentTitle = title, + ContentMessage = text, + ButtonDefinitions = ButtonEnum.Ok, + Icon = Icon.None, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + SizeToContent = SizeToContent.WidthAndHeight, + + HyperLinkParams = new HyperLinkParams + { + Text = link, + Action = new Action(() => GlobalActions.OpenHyperlinkAction(link)), + } + }; + } + // Executed by code behind view public async void OnLoaded() { @@ -191,9 +212,11 @@ await MessageBoxManager if (updateAvailable) { var box = MessageBoxManager - .GetMessageBoxStandard(Assets.Resources.Warning, - string.Format(Assets.Resources.NewAppReleaseAvailable, "https://github.com/xboxoneresearch/XboxPostcodeMonitor/releases"), ButtonEnum.Ok); - + .GetMessageBoxStandard(MsgBoxHyperlink( + Assets.Resources.Warning, + Assets.Resources.NewAppReleaseAvailable, + "https://github.com/xboxoneresearch/XboxPostcodeMonitor/releases" + )); await box.ShowAsync(); } } @@ -314,9 +337,11 @@ await MessageBoxManager if (updateAvailable) { var box = MessageBoxManager - .GetMessageBoxStandard(Assets.Resources.Warning, - string.Format(Assets.Resources.NewFirmwareReleaseAvailable, "https://github.com/xboxoneresearch/PicoDurangoPOST/releases"), ButtonEnum.Ok); - + .GetMessageBoxStandard(MsgBoxHyperlink( + Assets.Resources.Warning, + Assets.Resources.NewFirmwareReleaseAvailable, + "https://github.com/xboxoneresearch/PicoDurangoPOST/releases" + )); await box.ShowAsync(); } } diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs index f542dbf..752bf94 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs @@ -3,6 +3,7 @@ using PostCodeSerialMonitor.ViewModels; using System.Diagnostics; using System; +using PostCodeSerialMonitor.Utils; namespace PostCodeSerialMonitor.Views; @@ -79,11 +80,7 @@ private void OnHyperlinkClick(object sender, RoutedEventArgs e) { try { - Process.Start(new ProcessStartInfo - { - FileName = url, - UseShellExecute = true - }); + GlobalActions.OpenHyperlinkAction(url); } catch (Exception ex) { From 88b51033d319ae17ce38ff49f5ed7447993fead3 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:46:41 +0200 Subject: [PATCH 05/25] fix: dedup serial port names --- PostCodeSerialMonitor/Services/SerialService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PostCodeSerialMonitor/Services/SerialService.cs b/PostCodeSerialMonitor/Services/SerialService.cs index ea5742d..d003f1f 100644 --- a/PostCodeSerialMonitor/Services/SerialService.cs +++ b/PostCodeSerialMonitor/Services/SerialService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO.Ports; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -31,7 +32,8 @@ public SerialService(ILogger logger) public IEnumerable GetPortNames() { - return SerialPort.GetPortNames(); + // Filtering for unique port names via hashset conversion + return SerialPort.GetPortNames().ToHashSet(); } public bool IsOpen => _serialPort?.IsOpen ?? false; From cd91701918f3d6a00048c1824dd0e8e802bd4b18 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:58:43 +0200 Subject: [PATCH 06/25] feat: Show more details about COM port in tooltip --- PostCodeSerialMonitor/Models/PortInfo.cs | 6 +++ .../PostCodeSerialMonitor.csproj | 1 + .../Services/SerialService.cs | 52 +++++++++++++++++-- .../ViewModels/MainWindowViewModel.cs | 8 +-- PostCodeSerialMonitor/Views/MainWindow.axaml | 8 ++- 5 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 PostCodeSerialMonitor/Models/PortInfo.cs diff --git a/PostCodeSerialMonitor/Models/PortInfo.cs b/PostCodeSerialMonitor/Models/PortInfo.cs new file mode 100644 index 0000000..ba73491 --- /dev/null +++ b/PostCodeSerialMonitor/Models/PortInfo.cs @@ -0,0 +1,6 @@ +namespace PostCodeSerialMonitor.Models; + +public record PortInfo(string Name, string? Description) +{ + public override string ToString() => Name; +} diff --git a/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj b/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj index 47b46fe..fd032f2 100644 --- a/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj +++ b/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj @@ -64,5 +64,6 @@ + diff --git a/PostCodeSerialMonitor/Services/SerialService.cs b/PostCodeSerialMonitor/Services/SerialService.cs index d003f1f..9914cdf 100644 --- a/PostCodeSerialMonitor/Services/SerialService.cs +++ b/PostCodeSerialMonitor/Services/SerialService.cs @@ -2,11 +2,20 @@ using System.Collections.Generic; using System.IO.Ports; using System.Linq; +using System.Management; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; - +using PostCodeSerialMonitor.Models; namespace PostCodeSerialMonitor.Services; + +public static partial class SerialPortRegex { + [GeneratedRegex(@"\(COM\d+\)$", RegexOptions.IgnoreCase)] + public static partial Regex Windows(); +} + public class SerialService : IDisposable { private readonly ILogger _logger; @@ -30,12 +39,48 @@ public SerialService(ILogger logger) _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public IEnumerable GetPortNames() + public static IEnumerable GetPortNames() { // Filtering for unique port names via hashset conversion return SerialPort.GetPortNames().ToHashSet(); } + public IEnumerable GetPortInfos() + { + var descriptions = OperatingSystem.IsWindows() + ? GetWindowsPortDescriptions() + : []; + + return GetPortNames().Select(name => new PortInfo(name, descriptions.GetValueOrDefault(name))); + } + + [SupportedOSPlatform("windows")] + private Dictionary GetWindowsPortDescriptions() + { + var result = new Dictionary(); + try + { + using var searcher = new ManagementObjectSearcher( + "SELECT Name FROM Win32_PnPEntity WHERE Name LIKE '%(COM%'"); + foreach (ManagementBaseObject device in searcher.Get()) + { + if (device["Name"] is not string name) + continue; + + var match = SerialPortRegex.Windows().Match(name); + if (!match.Success) + continue; + + result[match.Value.Trim('(', ')')] = name[..match.Index].Trim(); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to query WMI for serial port descriptions"); + } + return result; + } + public bool IsOpen => _serialPort?.IsOpen ?? false; public async Task GoToREPL() @@ -80,7 +125,8 @@ public async Task ConnectAsync(string portName, int baudRate = 115200) if (!success) { Disconnect(); - throw new Exception(Assets.Resources.FailedFwVersion);} + throw new Exception(Assets.Resources.FailedFwVersion); + } // Get config state _serialPort.WriteLine("config"); diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index be7e798..875df72 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -33,7 +33,7 @@ public partial class MainWindowViewModel : ViewModelBase private GithubUpdateService _githubUpdateService; private IStorageProvider? _storageProvider; - public ObservableCollection SerialPorts { get; } = new(); + public ObservableCollection SerialPorts { get; } = new(); public ObservableCollection ConsoleModels { get; } = new(); @@ -47,7 +47,7 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanToggleConnection))] - private string? selectedPort; + private PortInfo? selectedPort; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanToggleConnection))] @@ -298,7 +298,7 @@ await MessageBoxManager private void RefreshPorts() { SerialPorts.Clear(); - foreach (var port in _serialService.GetPortNames()) + foreach (var port in _serialService.GetPortInfos()) SerialPorts.Add(port); if (SerialPorts.Count > 0 && SelectedPort == null) SelectedPort = SerialPorts.FirstOrDefault(); @@ -317,7 +317,7 @@ private async Task ToggleConnectionAsync() { try { - await _serialService.ConnectAsync(SelectedPort); + await _serialService.ConnectAsync(SelectedPort.Name); RawLogEntries?.Clear(); LogEntries?.Clear(); IsConnected = true; diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml index 842ce77..c5b8ae9 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml @@ -30,7 +30,13 @@ + IsEnabled="{Binding IsConnected, Converter={StaticResource BooleanNegationConverter}}"> + + + + + + - + + + + + + + + + + + + + + + + + + + + + @@ -153,18 +189,6 @@ - From 57bbd76c24ff08e09cbfbacf1dafd2691d3cb599 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:08:25 +0200 Subject: [PATCH 11/25] meta: Disable CS0162: Unreachable code warning for debug codeblock --- PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 5c87cff..b97014e 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -136,11 +136,14 @@ public MainWindowViewModel( _serialService.DeviceConfigChanged += OnDeviceConfigChanged; if (false) { +#pragma warning disable CS0162 // Unreachable code // For debugging UI layout PrefillDebugLogEntries(); +#pragma warning restore CS0162 // Unreachable code } } +#pragma warning disable CS0162 // Unreachable code private void PrefillDebugLogEntries() { for (int i = 0; i < 30; i++) @@ -161,6 +164,7 @@ private void PrefillDebugLogEntries() }); } } +#pragma warning restore CS0162 // Unreachable code private MessageBoxStandardParams MsgBoxHyperlink(string title, string text, string link) From 437ad7d0e894471861f2a18539dff598f0288a03 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:41:38 +0200 Subject: [PATCH 12/25] fix?: Duplicate serial ports --- .../ViewModels/MainWindowViewModel.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index b97014e..f87129c 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -34,7 +34,7 @@ public partial class MainWindowViewModel : ViewModelBase private GithubUpdateService _githubUpdateService; private IStorageProvider? _storageProvider; - public ObservableCollection SerialPorts { get; } = new(); + public ObservableCollection SerialPorts { get; set; } = new(); public ObservableCollection ConsoleModels { get; } = new(); @@ -335,11 +335,9 @@ await MessageBoxManager [RelayCommand] private void RefreshPorts() { - SerialPorts.Clear(); - foreach (var port in _serialService.GetPortInfos()) - SerialPorts.Add(port); - if (SerialPorts.Count > 0 && SelectedPort == null) - SelectedPort = SerialPorts.FirstOrDefault(); + var ports = _serialService.GetPortInfos(); + SerialPorts = new(ports); + SelectedPort = SerialPorts?.FirstOrDefault(); } [RelayCommand] From e7bbae9ab7c16dd40bccc6af472104b1cc7e0e94 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:48:54 +0200 Subject: [PATCH 13/25] fix: NullReferenceException on serialport disconnect --- .../ViewModels/MainWindowViewModel.cs | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index f87129c..7985ebc 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -343,53 +343,52 @@ private void RefreshPorts() [RelayCommand] private async Task ToggleConnectionAsync() { - if (IsConnected) + if (SelectedPort == null) { - Disconnect(); return; } - if (SelectedPort != null) + try { - try + if (IsConnected) { - await _serialService.ConnectAsync(SelectedPort.Name); - RawLogEntries?.Clear(); - LogEntries?.Clear(); - IsConnected = true; + _serialService.Disconnect(); + IsConnected = false; } - catch (Exception ex) + else { - _logger.LogError(ex, Assets.Resources.ErrorConection); - await MessageBoxManager - .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.ErrorConectionMessageBoxError, ex.Message), - ButtonEnum.Ok) - .ShowAsync(); + await _serialService.ConnectAsync(SelectedPort.Name); + IsConnected = true; } - if (IsConnected && _configurationService.Config.CheckForFwUpdates) + RawLogEntries?.Clear(); + LogEntries?.Clear(); + } + catch (Exception ex) + { + _logger.LogError(ex, Assets.Resources.ErrorConection); + await MessageBoxManager + .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.ErrorConectionMessageBoxError, ex.Message), + ButtonEnum.Ok) + .ShowAsync(); + } + + if (IsConnected && _configurationService.Config.CheckForFwUpdates) + { + var updateAvailable = await _githubUpdateService.CheckForFirmwareUpdatesAsync(_serialService.FirmwareVersion); + if (updateAvailable) { - var updateAvailable = await _githubUpdateService.CheckForFirmwareUpdatesAsync(_serialService.FirmwareVersion); - if (updateAvailable) - { - var box = MessageBoxManager - .GetMessageBoxStandard(MsgBoxHyperlink( - Assets.Resources.Warning, - Assets.Resources.NewFirmwareReleaseAvailable, - "https://github.com/xboxoneresearch/PicoDurangoPOST/releases" - )); - await box.ShowAsync(); - } + var box = MessageBoxManager + .GetMessageBoxStandard(MsgBoxHyperlink( + Assets.Resources.Warning, + Assets.Resources.NewFirmwareReleaseAvailable, + "https://github.com/xboxoneresearch/PicoDurangoPOST/releases" + )); + await box.ShowAsync(); } } } - private void Disconnect() - { - _serialService.Disconnect(); - IsConnected = false; - } - private void OnDataReceived(string line) { RawLogEntries.Add(line); From 07743840a94004ac09acaa88ea2b090fb5c54f5a Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:06:00 +0200 Subject: [PATCH 14/25] Revert "fix?: Duplicate serial ports" This reverts commit 437ad7d0e894471861f2a18539dff598f0288a03. --- .../ViewModels/MainWindowViewModel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 7985ebc..69c5e67 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -34,7 +34,7 @@ public partial class MainWindowViewModel : ViewModelBase private GithubUpdateService _githubUpdateService; private IStorageProvider? _storageProvider; - public ObservableCollection SerialPorts { get; set; } = new(); + public ObservableCollection SerialPorts { get; } = new(); public ObservableCollection ConsoleModels { get; } = new(); @@ -335,9 +335,11 @@ await MessageBoxManager [RelayCommand] private void RefreshPorts() { - var ports = _serialService.GetPortInfos(); - SerialPorts = new(ports); - SelectedPort = SerialPorts?.FirstOrDefault(); + SerialPorts.Clear(); + foreach (var port in _serialService.GetPortInfos()) + SerialPorts.Add(port); + if (SerialPorts.Count > 0 && SelectedPort == null) + SelectedPort = SerialPorts.FirstOrDefault(); } [RelayCommand] From ef9a6b5efcb4a6eff228938f427fdaa545289bf9 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:56:44 +0200 Subject: [PATCH 15/25] tests: Test port name duplication bug --- .../SerialServiceTests.cs | 50 ++++++++++++++++++- .../PostCodeSerialMonitor.csproj | 4 ++ .../Services/SerialService.cs | 6 +-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/PostCodeSerialMonitor.Tests/SerialServiceTests.cs b/PostCodeSerialMonitor.Tests/SerialServiceTests.cs index e853e77..dcb0e0d 100644 --- a/PostCodeSerialMonitor.Tests/SerialServiceTests.cs +++ b/PostCodeSerialMonitor.Tests/SerialServiceTests.cs @@ -2,8 +2,10 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Moq; using PostCodeSerialMonitor.Models; +using PostCodeSerialMonitor.Services; using Xunit; namespace PostCodeSerialMonitor.Tests; @@ -16,8 +18,9 @@ public class SerialServiceTests public SerialServiceTests() { + var logger = new Mock>(); _mockSerialPort = new Mock(); - _serialService = new SerialService(); + _serialService = new SerialService(logger.Object); } [Fact] @@ -140,3 +143,48 @@ public async Task Connect_ShouldRetryOnFailedReset() } } */ + +public class SerialServiceDisconnectTests +{ + [Fact] + public async Task Disconnect_WhileReadLoopIsBlockedOnRead_FiresDisconnectedExactlyOnce() + { + // Arrange + var logger = new Mock>(); + var service = new SerialService(logger.Object); + + var readerBlocked = new ManualResetEventSlim(false); // ReadLoop has entered ReadChar() + var releaseReader = new ManualResetEventSlim(false); // Close() happened, ReadChar() may throw + + var mockPort = new Mock(); + mockPort.SetupGet(p => p.IsOpen).Returns(true); + mockPort.Setup(p => p.ReadChar()).Returns(() => + { + readerBlocked.Set(); + releaseReader.Wait(TimeSpan.FromSeconds(5)); + throw new IOException("Port closed while a read was pending"); + }); + mockPort.Setup(p => p.Close()).Callback(() => releaseReader.Set()); + + service._serialPort = mockPort.Object; + service._readCts = new CancellationTokenSource(); + + var disconnectedCount = 0; + service.Disconnected += () => Interlocked.Increment(ref disconnectedCount); + + var readLoopTask = Task.Run(() => service.ReadLoop(service._readCts.Token)); + + // Ensure ReadLoop is genuinely blocked inside ReadChar() before disconnecting, + // to reproduce "close happens while a read is pending" deterministically. + Assert.True(readerBlocked.Wait(TimeSpan.FromSeconds(2)), "ReadLoop never reached ReadChar()"); + + // Act: single call, same as MainWindowViewModel.ToggleConnectionAsync does + service.Disconnect(); + + // Wait for the background ReadLoop thread's catch block to finish too. + await readLoopTask; + + // Assert + Assert.Equal(1, disconnectedCount); + } +} diff --git a/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj b/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj index fd032f2..e451b3b 100644 --- a/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj +++ b/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj @@ -22,6 +22,10 @@ + + + + PublicResXFileCodeGenerator diff --git a/PostCodeSerialMonitor/Services/SerialService.cs b/PostCodeSerialMonitor/Services/SerialService.cs index 35ef311..5a7c252 100644 --- a/PostCodeSerialMonitor/Services/SerialService.cs +++ b/PostCodeSerialMonitor/Services/SerialService.cs @@ -21,8 +21,8 @@ public static partial class SerialPortRegex { public class SerialService : IDisposable { private readonly ILogger _logger; - private ISerialPort? _serialPort; - private CancellationTokenSource? _readCts; + internal ISerialPort? _serialPort; + internal CancellationTokenSource? _readCts; public event Action? DataReceived; public event Action? Disconnected; public event Action? DeviceStateChanged; @@ -280,7 +280,7 @@ public void Disconnect() PrintColors = false; } - private void ReadLoop(CancellationToken token) + internal void ReadLoop(CancellationToken token) { try { From 08dd7e83cf9fdff75e3da624a45bce9454f56e79 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:59:53 +0200 Subject: [PATCH 16/25] fix: Duplicate call to Disconnected handler --- PostCodeSerialMonitor/Services/SerialService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PostCodeSerialMonitor/Services/SerialService.cs b/PostCodeSerialMonitor/Services/SerialService.cs index 5a7c252..daa7d5a 100644 --- a/PostCodeSerialMonitor/Services/SerialService.cs +++ b/PostCodeSerialMonitor/Services/SerialService.cs @@ -298,7 +298,8 @@ internal void ReadLoop(CancellationToken token) } catch (Exception) { - Disconnected?.Invoke(); + if (!token.IsCancellationRequested) + Disconnected?.Invoke(); } } From 9a0041a4c2617814bad3d78fa86a5c5648d5835c Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:01:12 +0200 Subject: [PATCH 17/25] meta: Set app version to v9.9.9 for non-CI/dev builds --- PostCodeSerialMonitor/PostCodeSerialMonitor.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj b/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj index e451b3b..e56e277 100644 --- a/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj +++ b/PostCodeSerialMonitor/PostCodeSerialMonitor.csproj @@ -9,9 +9,9 @@ true false - 0.2.0 - 0.2.0.0 - 0.2.0.0 + 9.9.9 + 9.9.9.0 + 9.9.9.0 XboxResearch PostCode Serial Monitor Serial monitor for Xbox POST codes From 460dd1b27982ca40ba2d6070124b58145b4a6945 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:26:36 +0200 Subject: [PATCH 18/25] fix: Autoscroll --- .../AutoScrollTrackerTests.cs | 36 ++++++++++++++++ .../Utils/AutoScrollTracker.cs | 42 +++++++++++++++++++ .../Views/MainWindow.axaml.cs | 15 +++---- 3 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs create mode 100644 PostCodeSerialMonitor/Utils/AutoScrollTracker.cs diff --git a/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs b/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs new file mode 100644 index 0000000..abec6cc --- /dev/null +++ b/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs @@ -0,0 +1,36 @@ +using PostCodeSerialMonitor.Utils; +using Xunit; + +namespace PostCodeSerialMonitor.Tests; + +public class AutoScrollTrackerTests +{ + [Fact] + public void OnScrollChanged_ContentGrowsFasterThanScrollToEndCanCatchUp_StaysAutoScrolled() + { + var tracker = new AutoScrollTracker(); + + // Simulates a burst of fast-arriving log lines: new content arrives, so the caller asks + // whether it should scroll to the end (as MainWindow does on every ItemsRepeater layout + // update) and does so, which is what will trigger the ScrollChanged below. + Assert.True(tracker.ShouldScrollToEnd()); + + // Before that resulting ScrollChanged is processed, more lines already grew the extent + // further (104 -> 130) while the offset still reflects the smaller extent it was set + // against (54). The user never touched the scrollbar, so auto-scroll should not detach. + tracker.OnScrollChanged(offsetY: 54, extentHeight: 130, viewportHeight: 50); + + Assert.True(tracker.AutoScroll); + } + + [Fact] + public void OnScrollChanged_UserScrollsAwayWithoutScrollToEnd_Detaches() + { + var tracker = new AutoScrollTracker(); + + // No ShouldScrollToEnd() call precedes this - the user dragged the scrollbar up themselves. + tracker.OnScrollChanged(offsetY: 0, extentHeight: 130, viewportHeight: 50); + + Assert.False(tracker.AutoScroll); + } +} diff --git a/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs b/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs new file mode 100644 index 0000000..b81c554 --- /dev/null +++ b/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs @@ -0,0 +1,42 @@ +namespace PostCodeSerialMonitor.Utils; + +// Decides whether a log view should keep auto-scrolling to the bottom as new +// entries arrive, versus staying put because the user scrolled away. +public class AutoScrollTracker +{ + // Distance from the bottom (in pixels) still considered "at the bottom", to absorb layout jitter + // from item virtualization/resizing so autoscroll doesn't flicker on/off during normal updates. + private const double BottomThreshold = 4; + + // Set right before we call ScrollToEnd() ourselves, so the ScrollChanged it triggers isn't + // mistaken for the user scrolling away (which happens if content keeps growing between our + // call and that event, leaving the offset behind a moving extent). + private bool _isProgrammaticScroll; + + public bool AutoScroll { get; private set; } = true; + + public bool ShouldScrollToEnd() + { + if (!AutoScroll) + return false; + + _isProgrammaticScroll = true; + return true; + } + + public void OnScrollChanged(double offsetY, double extentHeight, double viewportHeight) + { + if (_isProgrammaticScroll) + { + _isProgrammaticScroll = false; + return; + } + + AutoScroll = offsetY >= extentHeight - viewportHeight - BottomThreshold; + } + + public void Reset() + { + AutoScroll = true; + } +} diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs index 8ea873c..9d630a4 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs @@ -9,7 +9,7 @@ namespace PostCodeSerialMonitor.Views; public partial class MainWindow : Window { - private bool _autoScroll = true; + private readonly AutoScrollTracker _autoScroll = new(); private ScrollViewer? _scrollViewer; private ItemsRepeater? _itemsRepeater; @@ -38,25 +38,20 @@ protected override void OnLoaded(RoutedEventArgs e) } } - // Distance from the bottom (in pixels) still considered "at the bottom", to absorb layout jitter - // from item virtualization/resizing so autoscroll doesn't flicker on/off during normal updates. - private const double BottomThreshold = 4; - private void OnScrollChanged(object? sender, ScrollChangedEventArgs e) { if (_scrollViewer == null) return; - var atBottom = _scrollViewer.Offset.Y >= _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - BottomThreshold; - _autoScroll = atBottom; + _autoScroll.OnScrollChanged(_scrollViewer.Offset.Y, _scrollViewer.Extent.Height, _scrollViewer.Viewport.Height); if (AutoScrollButton != null) { - AutoScrollButton.IsVisible = !atBottom; + AutoScrollButton.IsVisible = !_autoScroll.AutoScroll; } } private void OnItemsRepeaterLayoutUpdated(object? sender, EventArgs e) { - if (_autoScroll && _scrollViewer != null) + if (_autoScroll.ShouldScrollToEnd() && _scrollViewer != null) { _scrollViewer.ScrollToEnd(); } @@ -64,7 +59,7 @@ private void OnItemsRepeaterLayoutUpdated(object? sender, EventArgs e) private void OnAutoScrollButtonClick(object? sender, RoutedEventArgs e) { - _autoScroll = true; + _autoScroll.Reset(); if (AutoScrollButton != null) { AutoScrollButton.IsVisible = false; From 055f65728f1070bc8572a55ffef12979c7897525 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:36:38 +0200 Subject: [PATCH 19/25] fix: Only clear log on connect, not disconnect --- PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 69c5e67..9ab9968 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -360,11 +360,9 @@ private async Task ToggleConnectionAsync() else { await _serialService.ConnectAsync(SelectedPort.Name); + ClearLog(); IsConnected = true; } - - RawLogEntries?.Clear(); - LogEntries?.Clear(); } catch (Exception ex) { From fa6a46d58a0de0f5b83c85c1ee82a0a58e4a599d Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:48:23 +0200 Subject: [PATCH 20/25] fix: auto-scroll (for real this time?) --- .../AutoScrollTrackerTests.cs | 35 +++++++++++++------ .../Utils/AutoScrollTracker.cs | 28 ++++++--------- .../Views/MainWindow.axaml.cs | 2 +- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs b/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs index abec6cc..1545134 100644 --- a/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs +++ b/PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs @@ -10,26 +10,39 @@ public void OnScrollChanged_ContentGrowsFasterThanScrollToEndCanCatchUp_StaysAut { var tracker = new AutoScrollTracker(); - // Simulates a burst of fast-arriving log lines: new content arrives, so the caller asks - // whether it should scroll to the end (as MainWindow does on every ItemsRepeater layout - // update) and does so, which is what will trigger the ScrollChanged below. - Assert.True(tracker.ShouldScrollToEnd()); - - // Before that resulting ScrollChanged is processed, more lines already grew the extent - // further (104 -> 130) while the offset still reflects the smaller extent it was set - // against (54). The user never touched the scrollbar, so auto-scroll should not detach. + // Simulates a burst of fast-arriving log lines: ScrollToEnd() was called while the + // extent was still 104 (viewport 50), setting the offset to 54. Before that resulting + // ScrollChanged is processed, more lines already grew the extent further to 130. The + // offset only ever moved forward, so this is not the user scrolling away. tracker.OnScrollChanged(offsetY: 54, extentHeight: 130, viewportHeight: 50); Assert.True(tracker.AutoScroll); } [Fact] - public void OnScrollChanged_UserScrollsAwayWithoutScrollToEnd_Detaches() + public void OnScrollChanged_UserDragsScrollbarUp_Detaches() { var tracker = new AutoScrollTracker(); + tracker.OnScrollChanged(offsetY: 76, extentHeight: 130, viewportHeight: 50); // starts at bottom - // No ShouldScrollToEnd() call precedes this - the user dragged the scrollbar up themselves. - tracker.OnScrollChanged(offsetY: 0, extentHeight: 130, viewportHeight: 50); + // The offset moves backward - only a manual scrollbar drag does that. + tracker.OnScrollChanged(offsetY: 20, extentHeight: 130, viewportHeight: 50); + + Assert.False(tracker.AutoScroll); + } + + [Fact] + public void OnScrollChanged_UserDragsAwayAfterSustainedAutoScrolling_StillDetaches() + { + var tracker = new AutoScrollTracker(); + + // Content keeps growing, offset keeps chasing it forward - auto-scroll stays engaged. + tracker.OnScrollChanged(offsetY: 50, extentHeight: 104, viewportHeight: 50); + tracker.OnScrollChanged(offsetY: 76, extentHeight: 130, viewportHeight: 50); + Assert.True(tracker.AutoScroll); + + // The user then grabs the scrollbar and drags it up. + tracker.OnScrollChanged(offsetY: 20, extentHeight: 130, viewportHeight: 50); Assert.False(tracker.AutoScroll); } diff --git a/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs b/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs index b81c554..7f53857 100644 --- a/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs +++ b/PostCodeSerialMonitor/Utils/AutoScrollTracker.cs @@ -8,31 +8,25 @@ public class AutoScrollTracker // from item virtualization/resizing so autoscroll doesn't flicker on/off during normal updates. private const double BottomThreshold = 4; - // Set right before we call ScrollToEnd() ourselves, so the ScrollChanged it triggers isn't - // mistaken for the user scrolling away (which happens if content keeps growing between our - // call and that event, leaving the offset behind a moving extent). - private bool _isProgrammaticScroll; + private double _lastOffsetY; public bool AutoScroll { get; private set; } = true; - public bool ShouldScrollToEnd() - { - if (!AutoScroll) - return false; - - _isProgrammaticScroll = true; - return true; - } - public void OnScrollChanged(double offsetY, double extentHeight, double viewportHeight) { - if (_isProgrammaticScroll) + if (offsetY < _lastOffsetY) + { + // The offset moved backward. Our own auto-scroll (ScrollToEnd) never does that - + // it only ever moves forward, chasing a growing extent - so this can only be the + // user dragging the scrollbar up. + AutoScroll = false; + } + else if (offsetY >= extentHeight - viewportHeight - BottomThreshold) { - _isProgrammaticScroll = false; - return; + AutoScroll = true; } - AutoScroll = offsetY >= extentHeight - viewportHeight - BottomThreshold; + _lastOffsetY = offsetY; } public void Reset() diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs index 9d630a4..24046fe 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs @@ -51,7 +51,7 @@ private void OnScrollChanged(object? sender, ScrollChangedEventArgs e) private void OnItemsRepeaterLayoutUpdated(object? sender, EventArgs e) { - if (_autoScroll.ShouldScrollToEnd() && _scrollViewer != null) + if (_autoScroll.AutoScroll && _scrollViewer != null) { _scrollViewer.ScrollToEnd(); } From bfde05750be3b7fe351f694d7017831cbec269a2 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Thu, 9 Jul 2026 06:26:23 +0200 Subject: [PATCH 21/25] fix: Show dialogs as non-dismissable popups --- .../ConfigurationDialogViewModel.cs | 2 +- .../ViewModels/MainWindowViewModel.cs | 24 +++++++------------ .../ViewModels/ViewModelBase.cs | 12 +++++++++- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs b/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs index f94b827..11fe8df 100644 --- a/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs @@ -82,7 +82,7 @@ await _configurationService.UpdateConfigurationAsync(config => await MessageBoxManager .GetMessageBoxStandard(Assets.Resources.RestartRequired, string.Format(Assets.Resources.LanguageChangedPleaseRestart), ButtonEnum.Ok) - .ShowAsync(); + .ShowAsPopupAsync(GetParentWindow()); } } diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 9ab9968..fbd6991 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -199,7 +199,7 @@ public async void OnLoaded() ButtonEnum.YesNo ); - var result = await box.ShowAsync(); + var result = await box.ShowAsPopupAsync(GetParentWindow()); if (result.HasFlag(ButtonResult.Yes)) { @@ -212,7 +212,7 @@ public async void OnLoaded() _logger.LogError(ex, Assets.Resources.FailedUpdateMetadata); await MessageBoxManager .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.FailedUpdateMetadataMessageBoxError, ex.Message), ButtonEnum.Ok) - .ShowAsync(); + .ShowAsPopupAsync(GetParentWindow()); } } } @@ -228,7 +228,7 @@ await MessageBoxManager .GetMessageBoxStandard(Assets.Resources.Warning, Assets.Resources.FailedLoadLocalMetadataMessageBoxWarning, ButtonEnum.Ok); - await box.ShowAsync(); + await box.ShowAsPopupAsync(GetParentWindow()); } try @@ -241,7 +241,7 @@ await MessageBoxManager await MessageBoxManager .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.FailedLoadLocalMetadataMessageBoxError, ex.Message), ButtonEnum.Ok) - .ShowAsync(); + .ShowAsPopupAsync(GetParentWindow()); } if (_configurationService.Config.CheckForAppUpdates) @@ -255,7 +255,7 @@ await MessageBoxManager Assets.Resources.NewAppReleaseAvailable, "https://github.com/xboxoneresearch/XboxPostcodeMonitor/releases" )); - await box.ShowAsync(); + await box.ShowAsPopupAsync(GetParentWindow()); } } } @@ -328,7 +328,7 @@ private async Task SaveLogAsync() await MessageBoxManager .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.ErrorSavingLogFileMessageBoxError, ex.Message), ButtonEnum.Ok) - .ShowAsync(); + .ShowAsPopupAsync(GetParentWindow()); } } @@ -370,7 +370,7 @@ private async Task ToggleConnectionAsync() await MessageBoxManager .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.ErrorConectionMessageBoxError, ex.Message), ButtonEnum.Ok) - .ShowAsync(); + .ShowAsPopupAsync(GetParentWindow()); } if (IsConnected && _configurationService.Config.CheckForFwUpdates) @@ -384,7 +384,7 @@ await MessageBoxManager Assets.Resources.NewFirmwareReleaseAvailable, "https://github.com/xboxoneresearch/PicoDurangoPOST/releases" )); - await box.ShowAsync(); + await box.ShowAsPopupAsync(GetParentWindow()); } } } @@ -441,12 +441,4 @@ private async Task ShowConfigurationAsync() await dialog.ShowDialog(GetParentWindow()); } - - private Window GetParentWindow() - { - if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - return desktop?.MainWindow ?? throw new Exception(Assets.Resources.FailedGetMainWindow); - else - throw new Exception(Assets.Resources.FailedGetApplicationLifetime); - } } \ No newline at end of file diff --git a/PostCodeSerialMonitor/ViewModels/ViewModelBase.cs b/PostCodeSerialMonitor/ViewModels/ViewModelBase.cs index 47960c5..965d134 100644 --- a/PostCodeSerialMonitor/ViewModels/ViewModelBase.cs +++ b/PostCodeSerialMonitor/ViewModels/ViewModelBase.cs @@ -1,7 +1,17 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using CommunityToolkit.Mvvm.ComponentModel; namespace PostCodeSerialMonitor.ViewModels; public class ViewModelBase : ObservableObject { + internal Window GetParentWindow() + { + if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + return desktop?.MainWindow ?? throw new Exception(Assets.Resources.FailedGetMainWindow); + else + throw new Exception(Assets.Resources.FailedGetApplicationLifetime); + } } From bda0452ccae751bedd939df66e241bc6f00c8bf4 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:07:38 +0200 Subject: [PATCH 22/25] feat: Option to show timestamps in main window --- .../Assets/Resources.Designer.cs | 20 ++++++++++++++++++- .../Assets/Resources.pt-BR.resx | 8 ++++++++ PostCodeSerialMonitor/Assets/Resources.resx | 8 ++++++++ .../Models/AppConfiguration.cs | 1 + PostCodeSerialMonitor/Models/LogEntry.cs | 3 +++ .../ConfigurationDialogViewModel.cs | 5 +++++ .../ViewModels/MainWindowViewModel.cs | 8 +++++++- .../Views/ConfigurationDialog.axaml | 6 +++++- PostCodeSerialMonitor/Views/MainWindow.axaml | 17 +++++++++++++++- 9 files changed, 72 insertions(+), 4 deletions(-) diff --git a/PostCodeSerialMonitor/Assets/Resources.Designer.cs b/PostCodeSerialMonitor/Assets/Resources.Designer.cs index 49b4789..eaf77e5 100644 --- a/PostCodeSerialMonitor/Assets/Resources.Designer.cs +++ b/PostCodeSerialMonitor/Assets/Resources.Designer.cs @@ -547,7 +547,25 @@ public static string CheckFirmwareUpdates { return ResourceManager.GetString("CheckFirmwareUpdates", resourceCulture); } } - + + /// + /// In Views/ConfigurationDialog.axaml + /// + public static string LogSettings { + get { + return ResourceManager.GetString("LogSettings", resourceCulture); + } + } + + /// + /// In Views/ConfigurationDialog.axaml + /// + public static string ShowTimestamps { + get { + return ResourceManager.GetString("ShowTimestamps", resourceCulture); + } + } + /// /// In Views/MainWindow.axaml /// diff --git a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx index 084f2a6..518440a 100644 --- a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx +++ b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx @@ -333,6 +333,14 @@ Verifique atualizações de firmware In Views/ConfigurationDialog.axaml + + Configurações de log + In Views/ConfigurationDialog.axaml + + + Mostrar timestamps + In Views/ConfigurationDialog.axaml + URL de atualização do firmware In Views/ConfigurationDialog.axaml diff --git a/PostCodeSerialMonitor/Assets/Resources.resx b/PostCodeSerialMonitor/Assets/Resources.resx index a562bcd..b3f69db 100644 --- a/PostCodeSerialMonitor/Assets/Resources.resx +++ b/PostCodeSerialMonitor/Assets/Resources.resx @@ -333,6 +333,14 @@ Check for firmware updates In Views/ConfigurationDialog.axaml + + Log settings + In Views/ConfigurationDialog.axaml + + + Show timestamps + In Views/ConfigurationDialog.axaml + Firmware update URL In Views/ConfigurationDialog.axaml diff --git a/PostCodeSerialMonitor/Models/AppConfiguration.cs b/PostCodeSerialMonitor/Models/AppConfiguration.cs index 1986d01..b2aacfc 100644 --- a/PostCodeSerialMonitor/Models/AppConfiguration.cs +++ b/PostCodeSerialMonitor/Models/AppConfiguration.cs @@ -12,6 +12,7 @@ public class AppConfiguration public bool CheckForAppUpdates { get; set; } = true; public bool CheckForCodeUpdates { get; set; } = true; public bool CheckForFwUpdates { get; set; } = true; + public bool ShowTimestamps { get; set; } = false; [Required] [Url] diff --git a/PostCodeSerialMonitor/Models/LogEntry.cs b/PostCodeSerialMonitor/Models/LogEntry.cs index 9b971be..a77b325 100644 --- a/PostCodeSerialMonitor/Models/LogEntry.cs +++ b/PostCodeSerialMonitor/Models/LogEntry.cs @@ -6,7 +6,10 @@ public class LogEntry { public string RawText { get; set; } = string.Empty; public DateTime Timestamp { get; set; } = DateTime.Now; + public string TimestampText => Timestamp.ToString("HH:mm:ss.fff"); public required DecodedCode DecodedCode { get; set; } + + public string FormattedWithTs => $"{TimestampText} {FormattedText}"; // CodeText + Description public string FormattedText => FormatText(); // Flavor, index and code (hex) diff --git a/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs b/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs index 11fe8df..0a6a410 100644 --- a/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/ConfigurationDialogViewModel.cs @@ -28,6 +28,9 @@ public partial class ConfigurationDialogViewModel : ViewModelBase [ObservableProperty] private bool checkForFwUpdates; + [ObservableProperty] + private bool showTimestamps; + [ObservableProperty] private string codesMetaBaseUrl; @@ -55,6 +58,7 @@ public ConfigurationDialogViewModel(ConfigurationService configurationService) CheckForAppUpdates = _originalConfiguration.CheckForAppUpdates; CheckForCodeUpdates = _originalConfiguration.CheckForCodeUpdates; CheckForFwUpdates = _originalConfiguration.CheckForFwUpdates; + ShowTimestamps = _originalConfiguration.ShowTimestamps; CodesMetaBaseUrl = _originalConfiguration.CodesMetaBaseUrl.ToString(); SelectedLanguage = _originalConfiguration.Language; @@ -72,6 +76,7 @@ await _configurationService.UpdateConfigurationAsync(config => config.CheckForAppUpdates = CheckForAppUpdates; config.CheckForCodeUpdates = CheckForCodeUpdates; config.CheckForFwUpdates = CheckForFwUpdates; + config.ShowTimestamps = ShowTimestamps; config.CodesMetaBaseUrl = new Uri(CodesMetaBaseUrl); config.Language = SelectedLanguage; }); diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index fbd6991..2a407ca 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -77,6 +77,9 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private bool printTimestamps; + [ObservableProperty] + private bool showTimestamps; + [ObservableProperty] private string i2cScanOutput = Assets.Resources.ScanButtonText; @@ -128,6 +131,7 @@ public MainWindowViewModel( } } SelectedConsoleModel = ConsoleModels.FirstOrDefault(); + ShowTimestamps = _configurationService.Config.ShowTimestamps; RefreshPorts(); _serialService.DataReceived += OnDataReceived; @@ -315,7 +319,7 @@ private async Task SaveLogAsync() sb.AppendLine("=== Decoded Log ==="); foreach (var entry in LogEntries.Where(e => e.DecodedCode != null)) { - sb.AppendLine(entry.FormattedText); + sb.AppendLine(entry.FormattedWithTs); } try @@ -440,5 +444,7 @@ private async Task ShowConfigurationAsync() }; await dialog.ShowDialog(GetParentWindow()); + + ShowTimestamps = _configurationService.Config.ShowTimestamps; } } \ No newline at end of file diff --git a/PostCodeSerialMonitor/Views/ConfigurationDialog.axaml b/PostCodeSerialMonitor/Views/ConfigurationDialog.axaml index d8ea325..ed42c1c 100644 --- a/PostCodeSerialMonitor/Views/ConfigurationDialog.axaml +++ b/PostCodeSerialMonitor/Views/ConfigurationDialog.axaml @@ -31,9 +31,13 @@ Watermark="{x:Static assets:Resources.CodeMetaUrl}" IsEnabled="{Binding CheckForCodeUpdates}"/> - + + + diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml index 7d75d36..6f11942 100644 --- a/PostCodeSerialMonitor/Views/MainWindow.axaml +++ b/PostCodeSerialMonitor/Views/MainWindow.axaml @@ -160,7 +160,21 @@ - + + + + + + + Date: Thu, 9 Jul 2026 20:55:54 +0200 Subject: [PATCH 23/25] feat: Add debug mode --- .../Assets/Resources.Designer.cs | 92 ++++++++++++++- .../Assets/Resources.pt-BR.resx | 40 +++++++ PostCodeSerialMonitor/Assets/Resources.resx | 40 +++++++ .../ViewModels/DebugDialogViewModel.cs | 107 ++++++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 58 +++++----- PostCodeSerialMonitor/Views/DebugDialog.axaml | 45 ++++++++ .../Views/DebugDialog.axaml.cs | 11 ++ PostCodeSerialMonitor/Views/MainWindow.axaml | 8 +- .../Views/MainWindow.axaml.cs | 8 ++ 9 files changed, 376 insertions(+), 33 deletions(-) create mode 100644 PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs create mode 100644 PostCodeSerialMonitor/Views/DebugDialog.axaml create mode 100644 PostCodeSerialMonitor/Views/DebugDialog.axaml.cs diff --git a/PostCodeSerialMonitor/Assets/Resources.Designer.cs b/PostCodeSerialMonitor/Assets/Resources.Designer.cs index eaf77e5..b6f92e6 100644 --- a/PostCodeSerialMonitor/Assets/Resources.Designer.cs +++ b/PostCodeSerialMonitor/Assets/Resources.Designer.cs @@ -655,7 +655,97 @@ public static string AppVersion { return ResourceManager.GetString("AppVersion", resourceCulture); } } - + + /// + /// In Views/MainWindow.axaml, Views/DebugDialog.axaml + /// + public static string Debug { + get { + return ResourceManager.GetString("Debug", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string FillDummyData { + get { + return ResourceManager.GetString("FillDummyData", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string Fill { + get { + return ResourceManager.GetString("Fill", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string DecodeStandaloneCode { + get { + return ResourceManager.GetString("DecodeStandaloneCode", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string CodeFlavorLabel { + get { + return ResourceManager.GetString("CodeFlavorLabel", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string ConsoleTypeLabel { + get { + return ResourceManager.GetString("ConsoleTypeLabel", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string Decode { + get { + return ResourceManager.GetString("Decode", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string Result { + get { + return ResourceManager.GetString("Result", resourceCulture); + } + } + + /// + /// In Views/DebugDialog.axaml + /// + public static string Close { + get { + return ResourceManager.GetString("Close", resourceCulture); + } + } + + /// + /// In ViewModels/DebugDialogViewModel.cs + /// + public static string InvalidCodeFormatMessageBoxError { + get { + return ResourceManager.GetString("InvalidCodeFormatMessageBoxError", resourceCulture); + } + } + /// /// In Views/MainWindow.axaml /// diff --git a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx index 518440a..3ef5923 100644 --- a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx +++ b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx @@ -381,6 +381,46 @@ Versão do App: In Views/MainWindow.axaml + + Depuração + In Views/MainWindow.axaml, Views/DebugDialog.axaml + + + Preencher com dados fictícios + In Views/DebugDialog.axaml + + + Preencher + In Views/DebugDialog.axaml + + + Decodificar código avulso + In Views/DebugDialog.axaml + + + Tipo de código + In Views/DebugDialog.axaml + + + Modelo do console + In Views/DebugDialog.axaml + + + Decodificar + In Views/DebugDialog.axaml + + + Resultado + In Views/DebugDialog.axaml + + + Fechar + In Views/DebugDialog.axaml + + + Formato de código inválido: {0} + In ViewModels/DebugDialogViewModel.cs + Visite XboxResearch.com In Views/MainWindow.axaml diff --git a/PostCodeSerialMonitor/Assets/Resources.resx b/PostCodeSerialMonitor/Assets/Resources.resx index b3f69db..a08d40b 100644 --- a/PostCodeSerialMonitor/Assets/Resources.resx +++ b/PostCodeSerialMonitor/Assets/Resources.resx @@ -381,6 +381,46 @@ App Version: In Views/MainWindow.axaml + + Debug + In Views/MainWindow.axaml, Views/DebugDialog.axaml + + + Fill view with dummy data + In Views/DebugDialog.axaml + + + Fill + In Views/DebugDialog.axaml + + + Decode standalone code + In Views/DebugDialog.axaml + + + Code flavor + In Views/DebugDialog.axaml + + + Console type + In Views/DebugDialog.axaml + + + Decode + In Views/DebugDialog.axaml + + + Result + In Views/DebugDialog.axaml + + + Close + In Views/DebugDialog.axaml + + + Invalid code format: {0} + In ViewModels/DebugDialogViewModel.cs + Visit XboxResearch.com In Views/MainWindow.axaml diff --git a/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs b/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs new file mode 100644 index 0000000..4744c37 --- /dev/null +++ b/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; +using PostCodeSerialMonitor.Models; +using PostCodeSerialMonitor.Services; + +namespace PostCodeSerialMonitor.ViewModels; + +public partial class DebugDialogViewModel : ViewModelBase +{ + private readonly ObservableCollection _logEntries; + private readonly SerialLineDecoder _serialLineDecoder; + + public ObservableCollection ConsoleTypes { get; } + public ObservableCollection CodeFlavors { get; } = new(new[] + { + CodeFlavor.SMC, CodeFlavor.SP, CodeFlavor.CPU, CodeFlavor.OS + }); + + [ObservableProperty] + private ConsoleType selectedConsoleType; + + [ObservableProperty] + private decimal entryCount = 20; + + [ObservableProperty] + private string codeInput = string.Empty; + + [ObservableProperty] + private string decodedResultText = string.Empty; + + public DebugDialogViewModel( + ObservableCollection logEntries, + SerialLineDecoder serialLineDecoder, + ObservableCollection consoleTypes) + { + _logEntries = logEntries; + _serialLineDecoder = serialLineDecoder; + ConsoleTypes = consoleTypes; + + SelectedConsoleType = ConsoleTypes.FirstOrDefault(); + } + + [RelayCommand] + private void FillDummyData() + { + for (int i = 0; i < (int)EntryCount; i++) + { + _logEntries.Add(new LogEntry + { + DecodedCode = new DecodedCode + { + Flavor = CodeFlavors[i % CodeFlavors.Count], + Index = i, + Code = i * 0x11, + SeverityLevel = (CodeSeverity)(i % 3), + Name = $"DEBUG_CODE_{i}", + Description = i % 4 == 0 + ? $"This is a long debug description for entry {i}, used to verify that the log window wraps text correctly and fills the full width of the resized main window instead of being clipped at a fixed maximum width." + : $"Debug entry {i}" + } + }); + } + } + + [RelayCommand] + private async Task DecodeStandaloneAsync() + { + int code; + try + { + var hex = CodeInput.Trim(); + if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + hex = hex[2..]; + code = Convert.ToInt32(hex, 16) & 0xFFFF; + } + catch (Exception) + { + await MessageBoxManager + .GetMessageBoxStandard(Assets.Resources.Error, string.Format(Assets.Resources.InvalidCodeFormatMessageBoxError, CodeInput), ButtonEnum.Ok) + .ShowAsPopupAsync(GetParentWindow()); + return; + } + + DecodedResultText = String.Empty; + foreach (var codeFlavor in CodeFlavors) + { + var line = $"{codeFlavor} (0): 0x{code:X4}"; + var decoded = _serialLineDecoder.DecodeLine(line, SelectedConsoleType); + DecodedResultText += decoded != null + ? new LogEntry { DecodedCode = decoded }.FormattedText + : $"Decoding '{line}' failed"; + DecodedResultText += "\n"; + } + } + + [RelayCommand] + private void Close(Avalonia.Controls.Window window) + { + window.Close(); + } +} diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs index 2a407ca..0b88c21 100644 --- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs +++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs @@ -95,6 +95,11 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private string appVersion; + [ObservableProperty] + private bool debugModeUnlocked; + + private int _appVersionClickCount; + public IStorageProvider? StorageProvider { get => _storageProvider; @@ -138,38 +143,7 @@ public MainWindowViewModel( _serialService.Disconnected += OnDisconnected; _serialService.DeviceStateChanged += OnDeviceStateChanged; _serialService.DeviceConfigChanged += OnDeviceConfigChanged; - - if (false) { -#pragma warning disable CS0162 // Unreachable code - // For debugging UI layout - PrefillDebugLogEntries(); -#pragma warning restore CS0162 // Unreachable code - } - } - -#pragma warning disable CS0162 // Unreachable code - private void PrefillDebugLogEntries() - { - for (int i = 0; i < 30; i++) - { - LogEntries.Add(new LogEntry - { - DecodedCode = new DecodedCode - { - Flavor = (CodeFlavor)(i % Enum.GetValues(typeof(CodeFlavor)).Length), - Index = i, - Code = i * 0x11, - SeverityLevel = (CodeSeverity)(i % 3), - Name = $"DEBUG_CODE_{i}", - Description = i % 4 == 0 - ? $"This is a long debug description for entry {i}, used to verify that the log window wraps text correctly and fills the full width of the resized main window instead of being clipped at a fixed maximum width." - : $"Debug entry {i}" - } - }); - } } -#pragma warning restore CS0162 // Unreachable code - private MessageBoxStandardParams MsgBoxHyperlink(string title, string text, string link) { @@ -447,4 +421,26 @@ private async Task ShowConfigurationAsync() ShowTimestamps = _configurationService.Config.ShowTimestamps; } + + [RelayCommand] + private void AppVersionClicked() + { + if (DebugModeUnlocked) + return; + + _appVersionClickCount++; + if (_appVersionClickCount >= 5) + DebugModeUnlocked = true; + } + + [RelayCommand] + private async Task ShowDebugMenuAsync() + { + var dialog = new DebugDialog + { + DataContext = new DebugDialogViewModel(LogEntries, _serialLineDecoder, ConsoleModels) + }; + + await dialog.ShowDialog(GetParentWindow()); + } } \ No newline at end of file diff --git a/PostCodeSerialMonitor/Views/DebugDialog.axaml b/PostCodeSerialMonitor/Views/DebugDialog.axaml new file mode 100644 index 0000000..092cfda --- /dev/null +++ b/PostCodeSerialMonitor/Views/DebugDialog.axaml @@ -0,0 +1,45 @@ + + + + + + + + +