The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:
- The engine reports the transmitter drop on its own thread.
WhenTransmitChanged took the state lock, decided the message had ended,
released the lock, and only then called Buffer.Ended(), which clears
what has gone to the engine. A macro pressed on the last character got
through StartAsync in that gap and had already flushed its own text into
Sent, so Ended() wiped the new text off the pane while the engine
transmitted it. Ended() is now called inside the same lock.
- The pane only started again when the engine reported a drop. Two macros
in a row keep the transmitter up, so that report never came and Sent
grew with every press. Everything in Sent is locked, because it is in
the engine and cannot be taken back, so the whole pane became
read-only. TypeAhead.Started() drops the last message's sent text and
keeps what was typed ahead, and StartAsync calls it whenever it keys a
new transmission.
The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
392 lines
16 KiB
C#
392 lines
16 KiB
C#
using Nonemm.Keying;
|
|
|
|
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 goes into the type-ahead buffer rather than to the engine whole,
|
|
/// so the part that has not gone out can still be rewritten. `Finished` is
|
|
/// raised when the buffer has run dry, with the last characters of the message
|
|
/// in the engine and about a third of a second of it still to go out.
|
|
///
|
|
/// The buffer is paced by the clock at `baud`, and the engine's own count of
|
|
/// what it has left is a brake on it.
|
|
///
|
|
/// Three states, which is what the transmitter can be doing:
|
|
///
|
|
/// | State | Key | Gate | Engine |
|
|
/// |---|---|---|---|
|
|
/// | `Down` | down | shut | holds nothing |
|
|
/// | `Keyed` | up | open, the feeder paces text over | holds `Ahead` characters |
|
|
/// | `Ending` | up | shut, everything left was flushed in one piece | holds the rest of the message |
|
|
///
|
|
/// | From | Event | To |
|
|
/// |---|---|---|
|
|
/// | `Down` | `Transmit`, or a message to send | `Keyed` |
|
|
/// | `Keyed` | more text | `Keyed` |
|
|
/// | `Keyed` | `{RX}` | `Ending` |
|
|
/// | `Keyed` | the engine drops with the message unfinished | `Keyed`, keyed again |
|
|
/// | `Ending` | the engine drops | `Down` |
|
|
/// | `Ending` | the engine never drops | `Down`, the key put down here |
|
|
/// | `Ending` | `Transmit`, or a message to send | `Keyed`, once the engine is empty and its stop is cleared |
|
|
/// | any | `AbortAsync` | `Down` |
|
|
///
|
|
/// The last two rows are why there is a state at all rather than a flag.
|
|
/// Ending a message takes as long as the engine takes to transmit what it
|
|
/// holds, and the operator can key again inside that time. An ending that went
|
|
/// on running put the key down in the middle of the message after it.
|
|
///
|
|
/// Coming back out of `Ending` is not free either. The stop is inside the
|
|
/// engine by then, waiting for its buffer to empty, and MMTTY fed while that
|
|
/// stands stayed keyed and transmitted nothing: the characters went in and
|
|
/// never came out. So a new message waits for what the engine holds to go out,
|
|
/// clears the stop with an abort, and keys again.
|
|
public sealed class DigitalEngineSender : MessageSender
|
|
{
|
|
/// How long the engine may make no progress at all, after it has been told
|
|
/// to stop, before the key goes down anyway. It is not a limit on the whole
|
|
/// wait: a flushed message is seconds of transmission and the engine is
|
|
/// entitled to all of it. Measured against the whole wait instead, it cut
|
|
/// a CQ off with 21 symbols still in the engine.
|
|
public static readonly TimeSpan StopPatience = TimeSpan.FromSeconds(1.5);
|
|
|
|
/// The longest the stop waits for the engine to say it has the message.
|
|
/// The stop does nothing at all if it arrives at an engine with an empty
|
|
/// buffer, so it goes out as soon as the count says there is something to
|
|
/// stop, and after this long whether the count says so or not. It is
|
|
/// N1MM's number, which N1MM sleeps outright.
|
|
public static readonly TimeSpan StopDelay = TimeSpan.FromMilliseconds(400);
|
|
|
|
private readonly DigitalEngine engine;
|
|
|
|
private readonly Lock gate = new();
|
|
|
|
/// Where the transmitter is, as far as this program knows.
|
|
private Keying state = Keying.Down;
|
|
|
|
/// Which transmission this is. It goes up whenever one starts or is
|
|
/// abandoned, and the ending of a message checks it after every step: an
|
|
/// ending belongs to one transmission and must not act on the next one.
|
|
/// Without it the ending of one message put the key down in the middle of
|
|
/// the message after it.
|
|
private int transmission;
|
|
|
|
public DigitalEngineSender(DigitalEngine engine, double baud = TypeAhead.DefaultBaud)
|
|
{
|
|
this.engine = engine;
|
|
Buffer = engine is EngineBuffer counter
|
|
? new TypeAhead(counter, baud)
|
|
: new TypeAhead(
|
|
(character, cancellation) => engine.SendAsync(character.ToString(), cancellation),
|
|
baud);
|
|
Buffer.Given += WhenGiven;
|
|
engine.TransmitChanged += WhenTransmitChanged;
|
|
}
|
|
|
|
/// What is waiting to go out, which the digital window shows and edits.
|
|
public TypeAhead Buffer { get; }
|
|
|
|
public bool IsReady => engine.IsConnected;
|
|
|
|
/// The buffer says when the last character has been transmitted, and the
|
|
/// engine says when it has stopped transmitting.
|
|
public bool ReportsCompletion => true;
|
|
|
|
public event EventHandler? Finished;
|
|
|
|
/// A message to send. Text is the operator asking for the transmitter, so
|
|
/// it keys as well: text arriving while the last message was ending
|
|
/// abandons that ending, and text arriving with the transmitter down brings
|
|
/// it up rather than letting MMTTY key itself off the first character.
|
|
public async Task SendAsync(string text, CancellationToken cancellation = default)
|
|
{
|
|
await StartAsync(cancellation).ConfigureAwait(false);
|
|
Buffer.Append(text);
|
|
}
|
|
|
|
private async Task TransmitAsync()
|
|
{
|
|
await StartAsync().ConfigureAwait(false);
|
|
Buffer.Transmit();
|
|
}
|
|
|
|
/// The TX button, Ctrl+Enter and `{TX}`: the transmitter comes up and what
|
|
/// is in the pane goes out, and so does whatever is typed into it from now
|
|
/// on.
|
|
///
|
|
/// Keying belongs here rather than in the window so that it is always the
|
|
/// first thing the engine is told. MMTTY keys itself off a character it is
|
|
/// given while the transmitter is down, sends it and drops again, so a key
|
|
/// that arrives behind the text puts a keyed-up gap in the middle of a
|
|
/// message.
|
|
public void Transmit() => _ = TransmitAsync();
|
|
|
|
/// N1MM's `{RX}`: everything still waiting goes to the engine in one piece
|
|
/// and the engine is then asked to stop. A macro that ends with it goes out
|
|
/// in full, and so does anything the operator has typed ahead of it.
|
|
///
|
|
/// It does not wait for the feeder to hand the message over first. Waiting
|
|
/// is what made the stop useless: by the time the last character had gone
|
|
/// over, the engine was empty again, and MMTTY ignores a stop that reaches
|
|
/// it empty.
|
|
public void ReturnToReceiveWhenSent() => _ = StopAsync();
|
|
|
|
/// A transmission begins, or the one that was ending goes on. True when the
|
|
/// engine has to be keyed, which is every time but one already keyed and
|
|
/// still going.
|
|
private async Task StartAsync(CancellationToken cancellation = default)
|
|
{
|
|
bool ending;
|
|
lock (gate)
|
|
{
|
|
ending = state == Keying.Ending;
|
|
}
|
|
if (ending)
|
|
{
|
|
// the last message is ending and its stop is inside the engine,
|
|
// waiting for the buffer to empty. Feeding an engine with that
|
|
// standing left MMTTY keyed with nothing going out and the
|
|
// characters swallowed, so what it still holds is let out and the
|
|
// stop is cleared with an abort before it is keyed again
|
|
await WaitUntilAiredAsync(StopPatience).ConfigureAwait(false);
|
|
await engine.AbortAsync(cancellation).ConfigureAwait(false);
|
|
}
|
|
bool key;
|
|
lock (gate)
|
|
{
|
|
key = state != Keying.Keyed;
|
|
if (key)
|
|
{
|
|
transmission++;
|
|
}
|
|
state = Keying.Keyed;
|
|
}
|
|
if (key)
|
|
{
|
|
// the pane starts again on the message that has gone out. The
|
|
// engine reporting the drop does this too, but it never reports one
|
|
// when a message follows the last close enough to keep the
|
|
// transmitter up, and the pane then held the whole run with none of
|
|
// it editable
|
|
Buffer.Started();
|
|
await engine.KeyAsync(cancellation).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// Waits until the engine says it holds something, or `patience` runs out.
|
|
/// The stop does nothing at an engine with an empty buffer, so it goes out
|
|
/// as soon as there is something to stop. N1MM sleeps 400 ms here instead,
|
|
/// which is the same wait without the question: it never reads the count.
|
|
/// A sleep is also wrong on a short message — `TU` is 330 ms of air at
|
|
/// 45.45 baud, so 400 ms of it puts the stop back where it does nothing.
|
|
private async Task WaitUntilHoldingAsync(TimeSpan patience)
|
|
{
|
|
DateTime giveUp = DateTime.UtcNow + patience;
|
|
while (Buffer.EngineHolds <= 0 && DateTime.UtcNow < giveUp)
|
|
{
|
|
await Task.Delay(TypeAhead.PollInterval).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// Waits until the engine has transmitted everything it was given, or until
|
|
/// it has made no progress for `patience`.
|
|
///
|
|
/// `patience` is not a limit on the whole wait: a flushed message is
|
|
/// seconds of transmission and the engine is entitled to all of it. What it
|
|
/// catches is an engine that has stopped moving. Measured against the whole
|
|
/// wait, it cut a CQ off with 21 symbols still in the engine.
|
|
private async Task WaitUntilAiredAsync(TimeSpan patience)
|
|
{
|
|
if (Buffer.Outstanding <= 0 && Buffer.EngineHolds <= 0 && !Buffer.IsSending)
|
|
{
|
|
return;
|
|
}
|
|
TaskCompletionSource aired = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
void WhenAired(object? sender, EventArgs e) => aired.TrySetResult();
|
|
Buffer.Aired += WhenAired;
|
|
try
|
|
{
|
|
int holds = Buffer.EngineHolds;
|
|
int outstanding = Buffer.Outstanding;
|
|
DateTime giveUp = DateTime.UtcNow + patience;
|
|
while (!aired.Task.IsCompleted && DateTime.UtcNow < giveUp)
|
|
{
|
|
await Task.Delay(TypeAhead.PollInterval).ConfigureAwait(false);
|
|
if (Buffer.EngineHolds < holds || Buffer.Outstanding < outstanding)
|
|
{
|
|
holds = Buffer.EngineHolds;
|
|
outstanding = Buffer.Outstanding;
|
|
giveUp = DateTime.UtcNow + patience;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Buffer.Aired -= WhenAired;
|
|
}
|
|
}
|
|
|
|
/// True while `mine` is still the transmission being ended. False once
|
|
/// something has started another one, ended this one, or aborted.
|
|
private bool Ending(int mine)
|
|
{
|
|
lock (gate)
|
|
{
|
|
return state == Keying.Ending && transmission == mine;
|
|
}
|
|
}
|
|
|
|
/// Escape: what has not gone to the engine is dropped, and the engine drops
|
|
/// what it still holds.
|
|
public Task AbortAsync(CancellationToken cancellation = default)
|
|
{
|
|
lock (gate)
|
|
{
|
|
state = Keying.Down;
|
|
transmission++;
|
|
}
|
|
Buffer.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()
|
|
{
|
|
Buffer.Given -= WhenGiven;
|
|
Buffer.Dispose();
|
|
engine.TransmitChanged -= WhenTransmitChanged;
|
|
}
|
|
|
|
/// The message is finished when the last character has been handed to the
|
|
/// engine, not when the engine has transmitted it. What stands after
|
|
/// `{END}` runs there, `{RX}` among it, and `{RX}` has to reach the engine
|
|
/// while the engine still holds something to send.
|
|
private void WhenGiven(object? sender, EventArgs e) => Finished?.Invoke(this, EventArgs.Empty);
|
|
|
|
/// Tells the engine to stop, waits for it to transmit what it still holds,
|
|
/// and puts the key down.
|
|
///
|
|
/// This is N1MM's ending, which is not the same as its keying. N1MM hands
|
|
/// MMTTY the whole message with `SendString` and calls `SetMmttyPTT(1)`
|
|
/// with the message still in the engine, and MMTTY ends the transmission
|
|
/// itself at the last character. So the feeding stops here: what is left
|
|
/// goes over in one piece and the engine is asked to stop on a full buffer.
|
|
/// Nothing after the flush can be rewritten, which is what `{RX}` means.
|
|
///
|
|
/// What sits between the two is the count, not a sleep. Two of N1MM's three
|
|
/// MMTTY send paths call the stop straight after the text and the third
|
|
/// sleeps 400 ms first, which is a fixed wait for something this program
|
|
/// can ask about: the stop goes out as soon as the engine says it holds
|
|
/// something. A fixed sleep is also wrong on a short message — `TU` is
|
|
/// 330 ms of air at 45.45 baud, so 400 ms of sleeping puts the stop back
|
|
/// where it does nothing.
|
|
///
|
|
/// The key going down is the fallback for an engine that ignores the stop,
|
|
/// which is what MMTTY did every time it was asked on an empty buffer. It
|
|
/// waits for the engine's own count to reach 0 and one symbol time on
|
|
/// top, which covers the count being read every `TypeAhead.PollInterval`
|
|
/// rather than the transmission. Every millisecond here is turnaround time
|
|
/// in a contest.
|
|
private async Task StopAsync()
|
|
{
|
|
int mine;
|
|
lock (gate)
|
|
{
|
|
if (state == Keying.Down)
|
|
{
|
|
return;
|
|
}
|
|
state = Keying.Ending;
|
|
mine = transmission;
|
|
}
|
|
string rest = await Buffer
|
|
.FlushAsync((text, cancellation) => engine.SendAsync(text, cancellation))
|
|
.ConfigureAwait(false);
|
|
if (rest.Length > 0 && Ending(mine))
|
|
{
|
|
await WaitUntilHoldingAsync(StopDelay).ConfigureAwait(false);
|
|
}
|
|
if (!Ending(mine))
|
|
{
|
|
return;
|
|
}
|
|
await engine.ReturnToReceiveAsync().ConfigureAwait(false);
|
|
await WaitUntilAiredAsync(StopPatience).ConfigureAwait(false);
|
|
if (!Ending(mine))
|
|
{
|
|
return;
|
|
}
|
|
await Task.Delay(Buffer.SymbolTime).ConfigureAwait(false);
|
|
if (Ending(mine))
|
|
{
|
|
await engine.ReleaseKeyAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// The engine dropping the transmitter ends the message, but only after
|
|
/// `{RX}` or an abort asked it to stop. The pane then starts again: what
|
|
/// has gone out is cleared, what the operator typed ahead is kept, and
|
|
/// nothing more is fed until the transmitter is keyed again.
|
|
///
|
|
/// A drop nobody asked for is the engine keying itself off what it is
|
|
/// given, which happens between the characters of a message that is still
|
|
/// going out, and is not the end of anything.
|
|
private void WhenTransmitChanged(object? sender, bool transmitting)
|
|
{
|
|
if (transmitting)
|
|
{
|
|
return;
|
|
}
|
|
bool unfinished = Buffer.IsTransmitting && Buffer.IsSending;
|
|
bool ended;
|
|
lock (gate)
|
|
{
|
|
ended = state == Keying.Ending;
|
|
if (ended)
|
|
{
|
|
state = Keying.Down;
|
|
transmission++;
|
|
// inside the lock, so a message starting at this moment cannot
|
|
// have its own text cleared by the end of the one before it.
|
|
// The drop arrives from the engine on its own thread: a macro
|
|
// pressed on the last character of a message got as far as
|
|
// handing its text to the engine before this ran, and the pane
|
|
// was then wiped while the text went out
|
|
Buffer.Ended();
|
|
}
|
|
}
|
|
if (!ended)
|
|
{
|
|
// the engine dropped in the middle of a message. What is left of it
|
|
// would go out into a transmitter that is down, so it comes back
|
|
// up; the engine keying itself off the next character would leave
|
|
// that character half sent
|
|
if (unfinished)
|
|
{
|
|
_ = engine.KeyAsync();
|
|
}
|
|
return;
|
|
}
|
|
if (!Buffer.IsSending)
|
|
{
|
|
Finished?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
/// Where the transmitter is. `Ending` is one message: everything left of it
|
|
/// has gone to the engine and nothing more is fed, and the engine is
|
|
/// transmitting what it holds.
|
|
private enum Keying
|
|
{
|
|
Down,
|
|
Keyed,
|
|
Ending,
|
|
}
|
|
}
|