using System.IO.Ports; using System.Text; namespace Nonemm.Keying; /// Sends CW through a WinKeyer on a serial port. The keyer is opened in host /// mode, which is the state where it takes text rather than following a paddle /// alone. public sealed class WinkeyerSender : MessageSender { private const byte AdminCommand = 0x00; private const byte HostOpen = 0x02; private const byte HostClose = 0x03; private const byte SetSpeed = 0x02; private const byte ClearBuffer = 0x0A; private readonly SerialPort port; private readonly WinkeyerStatus status = new(); public WinkeyerSender(string portName, int baudRate = 1200) { port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.Two); port.DataReceived += (_, _) => ReadStatus(); } public bool IsReady => port.IsOpen; /// The keyer sends a status byte of its own accord whenever it starts or /// stops sending, so nothing has to be asked for. public bool ReportsCompletion => true; public event EventHandler? Finished; /// Opens the port and puts the keyer in host mode. The keyer answers with /// its firmware version, which is read and thrown away. public void Open() { port.Open(); port.Write([AdminCommand, HostOpen], 0, 2); Thread.Sleep(100); port.DiscardInBuffer(); } public Task SendAsync(string text, CancellationToken cancellation = default) { Write(Encoding.ASCII.GetBytes(text.ToUpperInvariant())); return Task.CompletedTask; } public Task AbortAsync(CancellationToken cancellation = default) { Write([ClearBuffer]); return Task.CompletedTask; } public Task SetSpeedAsync(int wordsPerMinute, CancellationToken cancellation = default) { Write([SetSpeed, (byte)Math.Clamp(wordsPerMinute, 5, 99)]); return Task.CompletedTask; } public void Dispose() { if (port.IsOpen) { port.Write([AdminCommand, HostClose], 0, 2); port.Close(); } port.Dispose(); } /// Runs on the serial port's own thread. private void ReadStatus() { int waiting = port.BytesToRead; for (int at = 0; at < waiting; at++) { if (status.Read((byte)port.ReadByte())) { Finished?.Invoke(this, EventArgs.Empty); } } } private void Write(byte[] message) { if (!port.IsOpen) { throw new InvalidOperationException($"the keyer on {port.PortName} is not open"); } port.Write(message, 0, message.Length); } }