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}}"/>
-
-
+
Date: Wed, 8 Jul 2026 08:32:13 +0200
Subject: [PATCH 02/25] feat: Add clear-log-button
---
PostCodeSerialMonitor/Assets/Resources.Designer.cs | 9 +++++++++
PostCodeSerialMonitor/Assets/Resources.pt-BR.resx | 4 ++++
PostCodeSerialMonitor/Assets/Resources.resx | 4 ++++
PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs | 7 +++++++
PostCodeSerialMonitor/Views/MainWindow.axaml | 2 ++
5 files changed, 26 insertions(+)
diff --git a/PostCodeSerialMonitor/Assets/Resources.Designer.cs b/PostCodeSerialMonitor/Assets/Resources.Designer.cs
index 03b363c..9e55733 100644
--- a/PostCodeSerialMonitor/Assets/Resources.Designer.cs
+++ b/PostCodeSerialMonitor/Assets/Resources.Designer.cs
@@ -619,6 +619,15 @@ public static string SaveLog {
return ResourceManager.GetString("SaveLog", resourceCulture);
}
}
+
+ ///
+ /// In Views/MainWindow.axaml
+ ///
+ public static string ClearLog {
+ get {
+ return ResourceManager.GetString("ClearLog", resourceCulture);
+ }
+ }
///
/// In Views/MainWindow.axaml
diff --git a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx
index 9afb5d7..4c8ecd7 100644
--- a/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx
+++ b/PostCodeSerialMonitor/Assets/Resources.pt-BR.resx
@@ -365,6 +365,10 @@
Salvar Log
In Views/MainWindow.axaml
+
+ Limpar Log
+ In Views/MainWindow.axaml
+
Versão do App:
In Views/MainWindow.axaml
diff --git a/PostCodeSerialMonitor/Assets/Resources.resx b/PostCodeSerialMonitor/Assets/Resources.resx
index ca22351..8c40caf 100644
--- a/PostCodeSerialMonitor/Assets/Resources.resx
+++ b/PostCodeSerialMonitor/Assets/Resources.resx
@@ -365,6 +365,10 @@
Save Log
In Views/MainWindow.axaml
+
+ Clear Log
+ In Views/MainWindow.axaml
+
App Version:
In Views/MainWindow.axaml
diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
index 8316dc4..88e9301 100644
--- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
+++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
@@ -199,6 +199,13 @@ await MessageBoxManager
}
}
+ [RelayCommand]
+ private void ClearLog()
+ {
+ LogEntries.Clear();
+ RawLogEntries.Clear();
+ }
+
[RelayCommand]
private async Task SaveLogAsync()
{
diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml
index fc82f9b..3020ccd 100644
--- a/PostCodeSerialMonitor/Views/MainWindow.axaml
+++ b/PostCodeSerialMonitor/Views/MainWindow.axaml
@@ -41,6 +41,8 @@
+
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}}">
+
+
+
+
+
+
Date: Wed, 8 Jul 2026 12:10:57 +0200
Subject: [PATCH 07/25] feat: Implement fetching PortInfo for linux too
---
.../Services/SerialService.cs | 49 ++++++++++++++++++-
1 file changed, 47 insertions(+), 2 deletions(-)
diff --git a/PostCodeSerialMonitor/Services/SerialService.cs b/PostCodeSerialMonitor/Services/SerialService.cs
index 9914cdf..35ef311 100644
--- a/PostCodeSerialMonitor/Services/SerialService.cs
+++ b/PostCodeSerialMonitor/Services/SerialService.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Management;
@@ -47,8 +49,8 @@ public static IEnumerable GetPortNames()
public IEnumerable GetPortInfos()
{
- var descriptions = OperatingSystem.IsWindows()
- ? GetWindowsPortDescriptions()
+ var descriptions = OperatingSystem.IsWindows() ? GetWindowsPortDescriptions()
+ : OperatingSystem.IsLinux() ? GetLinuxPortDescriptions()
: [];
return GetPortNames().Select(name => new PortInfo(name, descriptions.GetValueOrDefault(name)));
@@ -81,6 +83,49 @@ private Dictionary GetWindowsPortDescriptions()
return result;
}
+ [SupportedOSPlatform("linux")]
+ private Dictionary GetLinuxPortDescriptions()
+ {
+ var result = new Dictionary();
+ try
+ {
+ foreach (var path in SerialPort.GetPortNames())
+ {
+ // Two hops: /sys/class/tty/{port} is itself a symlink, so its "device" symlink's
+ // relative target must be resolved against the already-resolved class directory.
+ var classDir = new DirectoryInfo($"/sys/class/tty/{Path.GetFileName(path)}")
+ .ResolveLinkTarget(returnFinalTarget: true) as DirectoryInfo;
+ if (classDir == null)
+ continue;
+
+ var dir = new DirectoryInfo(Path.Combine(classDir.FullName, "device"))
+ .ResolveLinkTarget(returnFinalTarget: true) as DirectoryInfo;
+
+ for (var i = 0; i < 5 && dir != null; i++, dir = dir.Parent)
+ {
+ var manufacturer = ReadSysfsAttribute(dir, "manufacturer");
+ var product = ReadSysfsAttribute(dir, "product");
+ if (manufacturer == null && product == null)
+ continue;
+
+ result[path] = string.Join(' ', new[] { manufacturer, product }.Where(s => !string.IsNullOrEmpty(s)));
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to read sysfs for serial port descriptions");
+ }
+ return result;
+ }
+
+ private static string? ReadSysfsAttribute(DirectoryInfo dir, string name)
+ {
+ var path = Path.Combine(dir.FullName, name);
+ return File.Exists(path) ? File.ReadAllText(path).Trim() : null;
+ }
+
public bool IsOpen => _serialPort?.IsOpen ?? false;
public async Task GoToREPL()
From a8228c122567ca1f3b6745788d699e3097d8356c Mon Sep 17 00:00:00 2001
From: tuxuser <462620+tuxuser@users.noreply.github.com>
Date: Wed, 8 Jul 2026 13:00:45 +0200
Subject: [PATCH 08/25] feat: Enable pre-filling log windows for testing
purposes
---
.../ViewModels/MainWindowViewModel.cs | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
index 875df72..6c17b24 100644
--- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
+++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
@@ -127,8 +127,35 @@ public MainWindowViewModel(
_serialService.Disconnected += OnDisconnected;
_serialService.DeviceStateChanged += OnDeviceStateChanged;
_serialService.DeviceConfigChanged += OnDeviceConfigChanged;
+
+ if (false) {
+ // For debugging UI layout
+ PrefillDebugLogEntries();
+ }
}
+ 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}"
+ }
+ });
+ }
+ }
+
+
private MessageBoxStandardParams MsgBoxHyperlink(string title, string text, string link)
{
return new MessageBoxStandardParams
From a16ae552aa55d76d17ce44e52a7eb146db60874f Mon Sep 17 00:00:00 2001
From: tuxuser <462620+tuxuser@users.noreply.github.com>
Date: Wed, 8 Jul 2026 13:01:17 +0200
Subject: [PATCH 09/25] fix: More robust auto-scroll behaviour
---
PostCodeSerialMonitor/Views/MainWindow.axaml.cs | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs
index 752bf94..8ea873c 100644
--- a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs
+++ b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs
@@ -38,18 +38,19 @@ 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;
- // If user scrolls up, disable autoscroll and show the button
- if (e.OffsetDelta.Y < 0)
+ var atBottom = _scrollViewer.Offset.Y >= _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - BottomThreshold;
+ _autoScroll = atBottom;
+ if (AutoScrollButton != null)
{
- _autoScroll = false;
- if (AutoScrollButton != null)
- {
- AutoScrollButton.IsVisible = true;
- }
+ AutoScrollButton.IsVisible = !atBottom;
}
}
From c34b6380563b9d5edf9db575acb7c91a83fbac8d Mon Sep 17 00:00:00 2001
From: tuxuser <462620+tuxuser@users.noreply.github.com>
Date: Wed, 8 Jul 2026 13:01:50 +0200
Subject: [PATCH 10/25] feat: Style improvements (button layout)
---
PostCodeSerialMonitor/App.axaml | 5 +
.../ViewModels/MainWindowViewModel.cs | 7 ++
PostCodeSerialMonitor/Views/MainWindow.axaml | 110 +++++++++++-------
3 files changed, 79 insertions(+), 43 deletions(-)
diff --git a/PostCodeSerialMonitor/App.axaml b/PostCodeSerialMonitor/App.axaml
index 89db5ab..c3829b7 100644
--- a/PostCodeSerialMonitor/App.axaml
+++ b/PostCodeSerialMonitor/App.axaml
@@ -9,6 +9,11 @@
M24 13.616v-3.183l-2.869-1.02c-.198-.687-.472-1.342-.811-1.955l1.308-2.751-2.257-2.258-2.752 1.307c-.613-.339-1.269-.613-1.955-.811l-1.021-2.869h-3.183l-1.021 2.869c-.686.198-1.342.472-1.955.811l-2.751-1.308-2.258 2.257 1.307 2.752c-.339.613-.614 1.268-.811 1.955l-2.869 1.02v3.183l2.869 1.02c.197.687.472 1.342.811 1.955l-1.308 2.752 2.257 2.258 2.752-1.308c.613.339 1.269.613 1.955.811l1.021 2.869h3.183l1.021-2.869c.687-.198 1.342-.472 1.955-.811l2.751 1.308 2.258-2.257-1.308-2.752c.339-.613.613-1.268.811-1.955l2.869-1.02zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z
M6.75 13.5H17.25C17.6642 13.5 18 13.8358 18 14.25C18 14.6297 17.7178 14.9435 17.3518 14.9932L17.25 15H6.75C6.33579 15 6 14.6642 6 14.25C6 13.8703 6.28215 13.5565 6.64823 13.5068L6.75 13.5Z M17.25 16.5H6.75L6.64823 16.5068C6.28215 16.5565 6 16.8703 6 17.25C6 17.6642 6.33579 18 6.75 18H17.25L17.3518 17.9932C17.7178 17.9435 18 17.6297 18 17.25C18 16.8358 17.6642 16.5 17.25 16.5Z M21 5.75C21 4.23122 19.7688 3 18.25 3H5.75C4.23122 3 3 4.23122 3 5.75V18.25C3 19.7688 4.23122 21 5.75 21H18.25C19.7688 21 21 19.7688 21 18.25V5.75ZM5.75 4.5H18.25C18.9404 4.5 19.5 5.05964 19.5 5.75V18.25C19.5 18.9404 18.9404 19.5 18.25 19.5H5.75C5.05964 19.5 4.5 18.9404 4.5 18.25V5.75C4.5 5.05964 5.05964 4.5 5.75 4.5Z
+ M7.74944331,5.18010908 C8.0006303,5.50946902 7.93725859,5.9800953 7.60789865,6.23128229 C5.81957892,7.59514774 4.75,9.70820889 4.75,12 C4.75,15.7359812 7.57583716,18.8119527 11.2066921,19.2070952 L10.5303301,18.5303301 C10.2374369,18.2374369 10.2374369,17.7625631 10.5303301,17.4696699 C10.7965966,17.2034034 11.2132603,17.1791973 11.5068718,17.3970518 L11.5909903,17.4696699 L13.5909903,19.4696699 C13.8572568,19.7359365 13.8814629,20.1526002 13.6636084,20.4462117 L13.5909903,20.5303301 L11.5909903,22.5303301 C11.298097,22.8232233 10.8232233,22.8232233 10.5303301,22.5303301 C10.2640635,22.2640635 10.2398575,21.8473998 10.4577119,21.5537883 L10.5303301,21.4696699 L11.280567,20.7208479 C6.78460951,20.3549586 3.25,16.5902554 3.25,12 C3.25,9.23526399 4.54178532,6.68321165 6.6982701,5.03856442 C7.02763004,4.78737743 7.49825632,4.85074914 7.74944331,5.18010908 Z M13.4696699,1.46966991 C13.7625631,1.76256313 13.7625631,2.23743687 13.4696699,2.53033009 L12.7204313,3.27923335 C17.2159137,3.64559867 20.75,7.4100843 20.75,12 C20.75,14.6444569 19.5687435,17.0974104 17.5691913,18.7491089 C17.2498402,19.0129038 16.7771069,18.9678666 16.513312,18.6485156 C16.2495171,18.3291645 16.2945543,17.8564312 16.6139054,17.5926363 C18.2720693,16.2229363 19.25,14.1922015 19.25,12 C19.25,8.26436254 16.4246828,5.18861329 12.7943099,4.7930139 L13.4696699,5.46966991 C13.7625631,5.76256313 13.7625631,6.23743687 13.4696699,6.53033009 C13.1767767,6.8232233 12.701903,6.8232233 12.4090097,6.53033009 L10.4090097,4.53033009 C10.1161165,4.23743687 10.1161165,3.76256313 10.4090097,3.46966991 L12.4090097,1.46966991 C12.701903,1.1767767 13.1767767,1.1767767 13.4696699,1.46966991 Z
+ M3 5.75C3 4.23122 4.23122 3 5.75 3H15.7145C16.5764 3 17.4031 3.34241 18.0126 3.9519L20.0481 5.98744C20.6576 6.59693 21 7.42358 21 8.28553V18.25C21 19.7688 19.7688 21 18.25 21H5.75C4.23122 21 3 19.7688 3 18.25V5.75ZM5.75 4.5C5.05964 4.5 4.5 5.05964 4.5 5.75V18.25C4.5 18.9404 5.05964 19.5 5.75 19.5H6V14.25C6 13.0074 7.00736 12 8.25 12H15.75C16.9926 12 18 13.0074 18 14.25V19.5H18.25C18.9404 19.5 19.5 18.9404 19.5 18.25V8.28553C19.5 7.8214 19.3156 7.37629 18.9874 7.0481L16.9519 5.01256C16.6918 4.75246 16.3582 4.58269 16 4.52344V7.25C16 8.49264 14.9926 9.5 13.75 9.5H9.25C8.00736 9.5 7 8.49264 7 7.25V4.5H5.75ZM16.5 19.5V14.25C16.5 13.8358 16.1642 13.5 15.75 13.5H8.25C7.83579 13.5 7.5 13.8358 7.5 14.25V19.5H16.5ZM8.5 4.5V7.25C8.5 7.66421 8.83579 8 9.25 8H13.75C14.1642 8 14.5 7.66421 14.5 7.25V4.5H8.5Z
+ M15.8698693,2.66881311 L20.838395,7.63733874 C21.7170746,8.5160184 21.7170746,9.9406396 20.838395,10.8193193 L12.1565953,19.4998034 L18.25448,19.5 C18.6341758,19.5 18.947971,19.7821539 18.9976334,20.1482294 L19.00448,20.25 C19.00448,20.6296958 18.7223262,20.943491 18.3562506,20.9931534 L18.25448,21 L9.84446231,21.0012505 C9.22825282,21.0348734 8.60085192,20.8163243 8.13013068,20.345603 L3.16160505,15.3770774 C2.28292539,14.4983977 2.28292539,13.0737765 3.16160505,12.1950969 L12.6878888,2.66881311 C13.5665685,1.79013346 14.9911897,1.79013346 15.8698693,2.66881311 Z M5.70859531,11.7678034 L4.22226522,13.255757 C3.929372,13.5486503 3.929372,14.023524 4.22226522,14.3164172 L9.19079085,19.2849428 C9.33723746,19.4313895 9.5291792,19.5046128 9.72112094,19.5046128 L9.75,19.5 L9.78849588,19.5015989 C9.95740385,19.4864544 10.1221581,19.4142357 10.251451,19.2849428 L11.7375953,17.7978034 L5.70859531,11.7678034 Z M13.748549,3.72947329 L6.76959531,10.7068034 L12.7985953,16.7368034 L19.7777348,9.75865909 C20.070628,9.46576587 20.070628,8.99089214 19.7777348,8.69799892 L14.8092091,3.72947329 C14.5163159,3.43658007 14.0414422,3.43658007 13.748549,3.72947329 Z
+ M13.7501344,8.41212026 L38.1671892,21.1169293 C39.7594652,21.9454306 40.3786269,23.9078584 39.5501255,25.5001344 C39.2420737,26.0921715 38.7592263,26.5750189 38.1671892,26.8830707 L13.7501344,39.5878797 C12.1578584,40.4163811 10.1954306,39.7972194 9.36692926,38.2049434 C9.12586301,37.7416442 9,37.2270724 9,36.704809 L9,11.295191 C9,9.50026556 10.4550746,8.045191 12.25,8.045191 C12.6976544,8.045191 13.1396577,8.13766178 13.5485655,8.31589049 L13.7501344,8.41212026 Z M12.5961849,10.629867 L12.4856981,10.5831892 C12.4099075,10.5581 12.3303482,10.545191 12.25,10.545191 C11.8357864,10.545191 11.5,10.8809774 11.5,11.295191 L11.5,36.704809 C11.5,36.8253313 11.5290453,36.9440787 11.584676,37.0509939 C11.7758686,37.4184422 12.2287365,37.5613256 12.5961849,37.370133 L37.0132397,24.665324 C37.1498636,24.5942351 37.2612899,24.4828088 37.3323788,24.3461849 C37.5235714,23.9787365 37.380688,23.5258686 37.0132397,23.334676 L12.5961849,10.629867 Z
+ M22.7399 6.32717C24.3781 8.48282 24.2132 11.571 22.2453 13.5389L20.3007 15.4835C20.0078 15.7764 19.533 15.7764 19.2401 15.4835L12.5226 8.76595C12.2297 8.47306 12.2297 7.99818 12.5226 7.70529L14.4671 5.76075C16.4352 3.79268 19.5237 3.62792 21.6793 5.26646L24.7238 2.22166C25.0167 1.92875 25.4916 1.92873 25.7845 2.22161C26.0774 2.51449 26.0774 2.98936 25.7845 3.28227L22.7399 6.32717ZM19.7704 13.8925L21.1846 12.4783C22.7467 10.9162 22.7467 8.3835 21.1846 6.82141C19.6225 5.25931 17.0899 5.25931 15.5278 6.82141L14.1135 8.23562L19.7704 13.8925Z M12.7778 11.215C13.0707 11.5079 13.0707 11.9828 12.7778 12.2757L10.6514 14.402L13.5982 17.3489L15.7238 15.2234C16.0167 14.9305 16.4916 14.9305 16.7844 15.2234C17.0773 15.5163 17.0773 15.9912 16.7844 16.284L14.6589 18.4095L15.4858 19.2364C15.7787 19.5293 15.7787 20.0042 15.4858 20.2971L13.5412 22.2416C11.5732 24.2096 8.48484 24.3745 6.32918 22.7361L3.28475 25.7808C2.99187 26.0737 2.517 26.0737 2.22409 25.7808C1.93118 25.488 1.93116 25.0131 2.22404 24.7202L5.26853 21.6754C3.63025 19.5197 3.79509 16.4314 5.76306 14.4635L7.7076 12.5189C8.0005 12.226 8.47537 12.226 8.76826 12.5189L9.59072 13.3414L11.7172 11.215C12.0101 10.9221 12.485 10.9221 12.7778 11.215ZM6.83028 21.1875C8.3929 22.7431 10.9207 22.7409 12.4806 21.181L13.8948 19.7668L8.23793 14.1099L6.82372 15.5241C5.26383 17.084 5.26163 19.6117 6.81709 21.1743L6.82366 21.1808L6.83028 21.1875Z
diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
index 6c17b24..5c87cff 100644
--- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
+++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
@@ -19,6 +19,7 @@
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
using MsBox.Avalonia.Dto;
+using Avalonia.Media;
namespace PostCodeSerialMonitor.ViewModels;
@@ -52,11 +53,17 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanToggleConnection))]
[NotifyPropertyChangedFor(nameof(ConnectionButtonText))]
+ [NotifyPropertyChangedFor(nameof(ConnectionButtonIcon))]
private bool isConnected;
public bool CanToggleConnection => IsConnected || SelectedPort != null;
public string ConnectionButtonText => IsConnected ? Assets.Resources.Disconnect : Assets.Resources.Connect;
+ public StreamGeometry? ConnectionButtonIcon =>
+ Avalonia.Application.Current?.Resources.TryGetResource(
+ IsConnected ? "plug_disconnected_regular" : "play_regular", null, out var resource) == true
+ ? resource as StreamGeometry
+ : null;
[ObservableProperty]
private int selectedTabIndex;
diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml
index c5b8ae9..7d75d36 100644
--- a/PostCodeSerialMonitor/Views/MainWindow.axaml
+++ b/PostCodeSerialMonitor/Views/MainWindow.axaml
@@ -26,37 +26,73 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PostCodeSerialMonitor/Views/DebugDialog.axaml.cs b/PostCodeSerialMonitor/Views/DebugDialog.axaml.cs
new file mode 100644
index 0000000..2e40f1b
--- /dev/null
+++ b/PostCodeSerialMonitor/Views/DebugDialog.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace PostCodeSerialMonitor.Views;
+
+public partial class DebugDialog : Window
+{
+ public DebugDialog()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml b/PostCodeSerialMonitor/Views/MainWindow.axaml
index 6f11942..5040a74 100644
--- a/PostCodeSerialMonitor/Views/MainWindow.axaml
+++ b/PostCodeSerialMonitor/Views/MainWindow.axaml
@@ -115,10 +115,16 @@
-
+
+
diff --git a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs
index 24046fe..3e90608 100644
--- a/PostCodeSerialMonitor/Views/MainWindow.axaml.cs
+++ b/PostCodeSerialMonitor/Views/MainWindow.axaml.cs
@@ -70,6 +70,14 @@ private void OnAutoScrollButtonClick(object? sender, RoutedEventArgs e)
}
}
+ private void OnAppVersionPointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
+ {
+ if (DataContext is MainWindowViewModel viewModel)
+ {
+ viewModel.AppVersionClickedCommand.Execute(null);
+ }
+ }
+
private void OnHyperlinkClick(object sender, RoutedEventArgs e)
{
if (sender is TextBlock textBlock && textBlock.Tag is string url)
From c852e1a4968787221f6e7cb6b49b309ef7c29bdb Mon Sep 17 00:00:00 2001
From: tuxuser <462620+tuxuser@users.noreply.github.com>
Date: Thu, 9 Jul 2026 22:08:05 +0200
Subject: [PATCH 24/25] fix: Trim rawLines before writing to log
---
PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
index 0b88c21..fbf3914 100644
--- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
+++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
@@ -285,7 +285,7 @@ private async Task SaveLogAsync()
sb.AppendLine("=== Raw Log ===");
foreach (var entry in RawLogEntries)
{
- sb.AppendLine(entry);
+ sb.AppendLine(entry?.Trim());
}
sb.AppendLine();
From ab600e041c46fa4e012d2054be246c15887563a8 Mon Sep 17 00:00:00 2001
From: tuxuser <462620+tuxuser@users.noreply.github.com>
Date: Thu, 9 Jul 2026 22:08:43 +0200
Subject: [PATCH 25/25] debug: Generate raw lines too
---
.../ViewModels/DebugDialogViewModel.cs | 15 ++++++++++++---
.../ViewModels/MainWindowViewModel.cs | 2 +-
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs b/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs
index 4744c37..47589cb 100644
--- a/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs
+++ b/PostCodeSerialMonitor/ViewModels/DebugDialogViewModel.cs
@@ -13,6 +13,7 @@ namespace PostCodeSerialMonitor.ViewModels;
public partial class DebugDialogViewModel : ViewModelBase
{
+ private readonly ObservableCollection _rawLogEntries;
private readonly ObservableCollection _logEntries;
private readonly SerialLineDecoder _serialLineDecoder;
@@ -35,10 +36,12 @@ public partial class DebugDialogViewModel : ViewModelBase
private string decodedResultText = string.Empty;
public DebugDialogViewModel(
+ ObservableCollection rawLogEntries,
ObservableCollection logEntries,
SerialLineDecoder serialLineDecoder,
ObservableCollection consoleTypes)
{
+ _rawLogEntries = rawLogEntries;
_logEntries = logEntries;
_serialLineDecoder = serialLineDecoder;
ConsoleTypes = consoleTypes;
@@ -51,13 +54,19 @@ private void FillDummyData()
{
for (int i = 0; i < (int)EntryCount; i++)
{
+ var code = i * 0x11;
+ var segment = i % 4;
+ var flavor = CodeFlavors[i % CodeFlavors.Count];
+
+ var rawLine = $"{flavor.ToString().PadRight(4)} ({segment}) 0x{code:X4}\r\n";
+ _rawLogEntries.Add(rawLine);
_logEntries.Add(new LogEntry
{
DecodedCode = new DecodedCode
{
- Flavor = CodeFlavors[i % CodeFlavors.Count],
- Index = i,
- Code = i * 0x11,
+ Flavor = flavor,
+ Index = segment,
+ Code = code,
SeverityLevel = (CodeSeverity)(i % 3),
Name = $"DEBUG_CODE_{i}",
Description = i % 4 == 0
diff --git a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
index fbf3914..a01d280 100644
--- a/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
+++ b/PostCodeSerialMonitor/ViewModels/MainWindowViewModel.cs
@@ -438,7 +438,7 @@ private async Task ShowDebugMenuAsync()
{
var dialog = new DebugDialog
{
- DataContext = new DebugDialogViewModel(LogEntries, _serialLineDecoder, ConsoleModels)
+ DataContext = new DebugDialogViewModel(RawLogEntries, LogEntries, _serialLineDecoder, ConsoleModels)
};
await dialog.ShowDialog(GetParentWindow());