Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f1a7cce
feat: Replace separate connect/disconnect buttons with single, contex…
tuxuser Jul 8, 2026
4869ac1
feat: Add clear-log-button
tuxuser Jul 8, 2026
e44eaf9
feat: Scale window width better, add 'consider donating' link in footer
tuxuser Jul 8, 2026
456fbb5
fix: make hyperlinks in messagebox clickable, unionize hyperlink hand…
tuxuser Jul 8, 2026
88b5103
fix: dedup serial port names
tuxuser Jul 8, 2026
cd91701
feat: Show more details about COM port in tooltip
tuxuser Jul 8, 2026
a332445
feat: Implement fetching PortInfo for linux too
tuxuser Jul 8, 2026
a8228c1
feat: Enable pre-filling log windows for testing purposes
tuxuser Jul 8, 2026
a16ae55
fix: More robust auto-scroll behaviour
tuxuser Jul 8, 2026
c34b638
feat: Style improvements (button layout)
tuxuser Jul 8, 2026
57bbd76
meta: Disable CS0162: Unreachable code warning for debug codeblock
tuxuser Jul 8, 2026
437ad7d
fix?: Duplicate serial ports
tuxuser Jul 8, 2026
e7bbae9
fix: NullReferenceException on serialport disconnect
tuxuser Jul 8, 2026
0774384
Revert "fix?: Duplicate serial ports"
tuxuser Jul 8, 2026
ef9a6b5
tests: Test port name duplication bug
tuxuser Jul 8, 2026
08dd7e8
fix: Duplicate call to Disconnected handler
tuxuser Jul 8, 2026
9a0041a
meta: Set app version to v9.9.9 for non-CI/dev builds
tuxuser Jul 8, 2026
460dd1b
fix: Autoscroll
tuxuser Jul 8, 2026
055f657
fix: Only clear log on connect, not disconnect
tuxuser Jul 8, 2026
fa6a46d
fix: auto-scroll (for real this time?)
tuxuser Jul 8, 2026
bfde057
fix: Show dialogs as non-dismissable popups
tuxuser Jul 9, 2026
bda0452
feat: Option to show timestamps in main window
tuxuser Jul 9, 2026
1246d99
feat: Add debug mode
tuxuser Jul 9, 2026
c852e1a
fix: Trim rawLines before writing to log
tuxuser Jul 9, 2026
ab600e0
debug: Generate raw lines too
tuxuser Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions PostCodeSerialMonitor.Tests/AutoScrollTrackerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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: 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_UserDragsScrollbarUp_Detaches()
{
var tracker = new AutoScrollTracker();
tracker.OnScrollChanged(offsetY: 76, extentHeight: 130, viewportHeight: 50); // starts at bottom

// 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);
}
}
50 changes: 49 additions & 1 deletion PostCodeSerialMonitor.Tests/SerialServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,8 +18,9 @@ public class SerialServiceTests

public SerialServiceTests()
{
var logger = new Mock<ILogger<SerialService>>();
_mockSerialPort = new Mock<ISerialPort>();
_serialService = new SerialService();
_serialService = new SerialService(logger.Object);
}

[Fact]
Expand Down Expand Up @@ -140,3 +143,48 @@ public async Task Connect_ShouldRetryOnFailedReset()
}
}
*/

public class SerialServiceDisconnectTests
{
[Fact]
public async Task Disconnect_WhileReadLoopIsBlockedOnRead_FiresDisconnectedExactlyOnce()
{
// Arrange
var logger = new Mock<ILogger<SerialService>>();
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<ISerialPort>();
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);
}
}
5 changes: 5 additions & 0 deletions PostCodeSerialMonitor/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
<Application.Resources>
<StreamGeometry x:Key="settings_regular">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</StreamGeometry>
<StreamGeometry x:Key="textbox_align_bottom_regular">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</StreamGeometry>
<StreamGeometry x:Key="arrow_sync_regular">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</StreamGeometry>
<StreamGeometry x:Key="save_regular">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</StreamGeometry>
<StreamGeometry x:Key="erase_regular">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</StreamGeometry>
<StreamGeometry x:Key="play_regular">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</StreamGeometry>
<StreamGeometry x:Key="plug_disconnected_regular">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</StreamGeometry>
</Application.Resources>

<Application.DataTemplates>
Expand Down
136 changes: 132 additions & 4 deletions PostCodeSerialMonitor/Assets/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading