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:
Erik
2026-08-27 11:23:30 +00:00
parent d96cbe146b
commit f5aa77ece3
25 changed files with 1002 additions and 26 deletions

View File

@@ -0,0 +1,49 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Nonemm.Keying;
/// Sends CW through `cwdaemon`, which keys the radio from a serial or parallel
/// port and is what Linux stations usually run. Its protocol is one UDP
/// datagram per command; escape sequences start with a 0x1B byte.
public sealed class CwDaemonSender : MessageSender
{
private readonly UdpClient socket = new();
private readonly IPEndPoint daemon;
public CwDaemonSender(string host = "127.0.0.1", int port = 6789)
{
daemon = new IPEndPoint(IPAddress.Parse(host), port);
IsReady = true;
}
public bool IsReady { get; private set; }
public Task SendAsync(string text, CancellationToken cancellation = default) =>
WriteAsync(Encoding.ASCII.GetBytes(text.ToUpperInvariant()), cancellation);
public Task AbortAsync(CancellationToken cancellation = default) =>
WriteAsync([0x1B, (byte)'4'], cancellation);
public Task SetSpeedAsync(int wordsPerMinute, CancellationToken cancellation = default) =>
WriteAsync(Escape('2', wordsPerMinute.ToString()), cancellation);
public void Dispose() => socket.Dispose();
private static byte[] Escape(char command, string argument) =>
[0x1B, (byte)command, .. Encoding.ASCII.GetBytes(argument)];
private async Task WriteAsync(byte[] message, CancellationToken cancellation)
{
try
{
await socket.SendAsync(message, daemon, cancellation).ConfigureAwait(false);
}
catch (SocketException e)
{
IsReady = false;
throw new InvalidOperationException($"could not reach cwdaemon at {daemon}: {e.Message}", e);
}
}
}