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, } }