Add CW keying, function key messages and the documentation
The function keys send through cwdaemon or a WinKeyer, with N1MM's message macros. Escape stops sending. Settings are read with the reflection serializer rather than a generated one: the generated one hands back null for every property the file leaves out instead of the value the property is declared with, which crashed the program the first time a new setting was added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
52
tests/Nonemm.Keying.Tests/CwDaemonSenderTests.cs
Normal file
52
tests/Nonemm.Keying.Tests/CwDaemonSenderTests.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Nonemm.Keying.Tests;
|
||||
|
||||
/// The sender is tested against a socket that speaks cwdaemon's side of the
|
||||
/// protocol, so what goes on the wire is what is asserted.
|
||||
public class CwDaemonSenderTests : IDisposable
|
||||
{
|
||||
private readonly UdpClient daemon = new(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
|
||||
private int Port => ((IPEndPoint)daemon.Client.LocalEndPoint!).Port;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
daemon.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task<byte[]> NextDatagram(Func<Task> send)
|
||||
{
|
||||
Task<UdpReceiveResult> receiving = daemon.ReceiveAsync();
|
||||
await send();
|
||||
UdpReceiveResult received = await receiving.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
return received.Buffer;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TextGoesOutInUpperCase()
|
||||
{
|
||||
using CwDaemonSender sender = new("127.0.0.1", Port);
|
||||
byte[] sent = await NextDatagram(() => sender.SendAsync("cq test de dl1abc"));
|
||||
Assert.Equal("CQ TEST DE DL1ABC", Encoding.ASCII.GetString(sent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AbortIsTheEscapeFourCommand()
|
||||
{
|
||||
using CwDaemonSender sender = new("127.0.0.1", Port);
|
||||
byte[] sent = await NextDatagram(() => sender.AbortAsync());
|
||||
Assert.Equal([0x1B, (byte)'4'], sent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SpeedIsTheEscapeTwoCommand()
|
||||
{
|
||||
using CwDaemonSender sender = new("127.0.0.1", Port);
|
||||
byte[] sent = await NextDatagram(() => sender.SetSpeedAsync(32));
|
||||
Assert.Equal([0x1B, (byte)'2', (byte)'3', (byte)'2'], sent);
|
||||
}
|
||||
}
|
||||
25
tests/Nonemm.Keying.Tests/Nonemm.Keying.Tests.csproj
Normal file
25
tests/Nonemm.Keying.Tests/Nonemm.Keying.Tests.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Nonemm.Keying\Nonemm.Keying.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
59
tests/Nonemm.Network.Tests/ContactMessageTests.cs
Normal file
59
tests/Nonemm.Network.Tests/ContactMessageTests.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Network.Tests;
|
||||
|
||||
public class ContactMessageTests
|
||||
{
|
||||
private static Qso Contact() => new()
|
||||
{
|
||||
Id = "0123456789abcdef0123456789abcdef",
|
||||
TimestampUtc = new DateTime(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc),
|
||||
Call = Callsign.Parse("JA1XYZ"),
|
||||
Frequency = Frequency.FromKilohertz(14_025.5),
|
||||
Mode = Modes.Cw,
|
||||
ContestName = "CQWW",
|
||||
ContestNumber = 3,
|
||||
SentReport = "599",
|
||||
ReceivedReport = "579",
|
||||
Zone = 25,
|
||||
Points = 3,
|
||||
IsMultiplier1 = true,
|
||||
CountryPrefix = "JA",
|
||||
Continent = "AS",
|
||||
WpxPrefix = "JA1",
|
||||
IsRunQso = true,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ContactSurvivesTheRoundTrip()
|
||||
{
|
||||
Qso? read = ContactMessage.Read(ContactMessage.Write(Contact(), "DL1ABC", "PC1"));
|
||||
|
||||
Assert.NotNull(read);
|
||||
Assert.Equal("0123456789abcdef0123456789abcdef", read.Id);
|
||||
Assert.Equal("JA1XYZ", read.Call.Text);
|
||||
Assert.Equal(14_025_500, read.Frequency.Hertz);
|
||||
Assert.Equal(new DateTime(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc), read.TimestampUtc);
|
||||
Assert.Equal(25, read.Zone);
|
||||
Assert.Equal("JA", read.CountryPrefix);
|
||||
Assert.True(read.IsRunQso);
|
||||
Assert.Equal("PC1", read.StationName);
|
||||
}
|
||||
|
||||
/// A contact that arrived over the network was made somewhere else, whatever
|
||||
/// the sender put in the field.
|
||||
[Fact]
|
||||
public void AContactFromTheNetworkIsNotOriginal() =>
|
||||
Assert.False(ContactMessage.Read(ContactMessage.Write(Contact(), "DL1ABC", "PC1"))!.IsOriginal);
|
||||
|
||||
[Fact]
|
||||
public void FrequenciesAreSentInUnitsOfTenHertz() =>
|
||||
Assert.Contains("<rxfreq>1402550</rxfreq>", ContactMessage.Write(Contact(), "DL1ABC", "PC1"));
|
||||
|
||||
[Theory]
|
||||
[InlineData("<radioinfo><app>N1MM</app></radioinfo>")]
|
||||
[InlineData("not xml at all")]
|
||||
[InlineData("<contactinfo><call></call></contactinfo>")]
|
||||
public void AnythingThatIsNotAContactIsPassedOver(string message) =>
|
||||
Assert.Null(ContactMessage.Read(message));
|
||||
}
|
||||
26
tests/Nonemm.Network.Tests/Nonemm.Network.Tests.csproj
Normal file
26
tests/Nonemm.Network.Tests/Nonemm.Network.Tests.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Nonemm.Network\Nonemm.Network.csproj" />
|
||||
<ProjectReference Include="..\..\src\Nonemm.Core\Nonemm.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
54
tests/Nonemm.Session.Tests/MessageExpanderTests.cs
Normal file
54
tests/Nonemm.Session.Tests/MessageExpanderTests.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Contests.Rules;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Storage;
|
||||
|
||||
namespace Nonemm.Session.Tests;
|
||||
|
||||
public class MessageExpanderTests
|
||||
{
|
||||
private static LoggingSession Session()
|
||||
{
|
||||
FakeLogStore store = new();
|
||||
ContestInstance instance = store.AddContest(new ContestInstance
|
||||
{
|
||||
ContestNumber = 0,
|
||||
ContestName = "CQWW",
|
||||
SentExchange = "14",
|
||||
});
|
||||
return new LoggingSession(
|
||||
store,
|
||||
new CqWorldWide(ModeCategory.Cw),
|
||||
instance,
|
||||
new StationInfo { Callsign = "DL1ABC", CqZone = 14, Name = "Erik" },
|
||||
null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MyCallAndTheirCallAreFilledIn()
|
||||
{
|
||||
LoggingSession session = Session();
|
||||
session.Entry.Call = "JA1XYZ";
|
||||
Assert.Equal("JA1XYZ DE DL1ABC", MessageExpander.Expand("{CALL} DE {MYCALL}", session));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheSentExchangeComesFromTheContestSetup() =>
|
||||
Assert.Equal("599 14", MessageExpander.Expand("{SENTRST} {EXCH}", Session()));
|
||||
|
||||
[Fact]
|
||||
public void HashIsTheSerialNumber() =>
|
||||
Assert.Equal("NR 1", MessageExpander.Expand("NR #", Session()));
|
||||
|
||||
[Fact]
|
||||
public void CutNumbersReplaceZeroAndNine() =>
|
||||
Assert.Equal("5NN", MessageExpander.Expand("{SENTRSTCUT}", Session()));
|
||||
|
||||
[Fact]
|
||||
public void AMacroThatIsNotKnownDisappears() =>
|
||||
Assert.Equal("TU ", MessageExpander.Expand("TU {NOTAMACRO}", Session()));
|
||||
|
||||
[Fact]
|
||||
public void AnUnclosedMacroIsLeftAsTyped() =>
|
||||
Assert.Equal("TU {MYCALL", MessageExpander.Expand("TU {MYCALL", Session()));
|
||||
}
|
||||
Reference in New Issue
Block a user