Hold a digital message back so it can still be changed

A digital engine takes a whole message and transmits it at 45.45 baud, which
takes several seconds. Once it has the message nothing can be changed, so an
operator who sees the wrong call going out has to stop the transmission and
start again.

TypeAhead keeps the message instead and feeds the engine one character at a
time. What has not gone out yet can be rewritten, added to or deleted. Cursor
is how much of it may go: the pump stops there and the engine idles on the air,
which is the diddle a RTTY engine sends between characters anyway.

The pump counts character times off the clock at the baud rate rather than
asking the engine what it has left, so it runs a little behind: a gap between
two characters is idle on the air and costs nothing, while feeding faster than
the engine transmits would put text out of reach again.

Every digital message goes through it — the function keys, ESM, the QTC window
and the digital window's own buttons — because they all send through
DigitalEngineSender. Finished is now raised when the buffer runs dry, which is
when the message has really gone, so what stands after {END} runs then and {RX}
drops the transmitter at the right moment. The transmitter dropping only counts
as the end when nothing is left to send: an engine that keys itself off what it
is given drops between two characters as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
2026-09-01 05:25:36 +00:00
parent 0e2aee545e
commit 65f1051bc1
3 changed files with 492 additions and 9 deletions

View File

@@ -5,40 +5,71 @@ namespace Nonemm.Digital;
/// The digital engine as something that sends a message, so a macro goes out
/// through the same expander, the same `{END}` handling and the same ESM as a
/// CW message does.
///
/// A message is not handed to the engine whole. It goes into the type-ahead
/// buffer, which feeds the engine a character at a time, so the operator can
/// still rewrite the part that has not gone out. `Finished` is raised when the
/// buffer runs dry, which is when the message has really gone.
public sealed class DigitalEngineSender : MessageSender
{
private readonly DigitalEngine engine;
public DigitalEngineSender(DigitalEngine engine)
public DigitalEngineSender(DigitalEngine engine, double baud = TypeAhead.DefaultBaud)
{
this.engine = engine;
TypeAhead = new TypeAhead(
(character, cancellation) => engine.SendAsync(character.ToString(), cancellation),
baud);
TypeAhead.Drained += WhenDrained;
engine.TransmitChanged += WhenTransmitChanged;
}
/// What is waiting to go out, which the digital window shows and edits.
public TypeAhead TypeAhead { get; }
public bool IsReady => engine.IsConnected;
/// The engine says when it has stopped transmitting, which is the same
/// thing a keyer reports.
/// The buffer says when the last character has gone to the engine, and the
/// engine says when it has stopped transmitting.
public bool ReportsCompletion => true;
public event EventHandler? Finished;
public Task SendAsync(string text, CancellationToken cancellation = default) =>
engine.SendAsync(text, cancellation);
public Task SendAsync(string text, CancellationToken cancellation = default)
{
TypeAhead.Append(text);
return Task.CompletedTask;
}
public Task AbortAsync(CancellationToken cancellation = default) =>
engine.AbortAsync(cancellation);
/// Escape and the RX button: what has not gone to the engine is dropped,
/// and the engine drops what it still holds.
public Task AbortAsync(CancellationToken cancellation = default)
{
TypeAhead.Drop();
return engine.AbortAsync(cancellation);
}
/// RTTY runs at the speed the engine is set to, so there is nothing to set
/// per message.
public Task SetSpeedAsync(int wordsPerMinute, CancellationToken cancellation = default) =>
Task.CompletedTask;
public void Dispose() => engine.TransmitChanged -= WhenTransmitChanged;
public void Dispose()
{
TypeAhead.Drained -= WhenDrained;
TypeAhead.Dispose();
engine.TransmitChanged -= WhenTransmitChanged;
}
private void WhenDrained(object? sender, EventArgs e) => Finished?.Invoke(this, EventArgs.Empty);
/// The engine dropping the transmitter ends the message, but only when
/// there is nothing left to send: an engine that keys itself off what it
/// is given drops between two characters of a message the pump is still
/// feeding, and that is not the end of anything.
private void WhenTransmitChanged(object? sender, bool transmitting)
{
if (!transmitting)
if (!transmitting && !TypeAhead.IsSending)
{
Finished?.Invoke(this, EventArgs.Empty);
}