Start the transmit pane again on every new message

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
This commit is contained in:
2026-09-03 14:40:30 +00:00
parent 7ae60be4a0
commit f49a8c10fd
20 changed files with 2604 additions and 390 deletions

View File

@@ -49,6 +49,9 @@ public sealed class EngineProbe
/// How long the engine is given to key up and to drop again.
private static readonly TimeSpan Keying = TimeSpan.FromSeconds(3);
/// One character at 45.45 baud, which is the rate the window feeds at.
private static readonly TimeSpan CharacterTime = TimeSpan.FromMilliseconds(165);
private const string Message = "CQ TEST DE OM5M OM5M ";
private const string Word = "ABCD";
@@ -80,6 +83,140 @@ public sealed class EngineProbe
await MessageAsync(cancellation).ConfigureAwait(false);
await WordAsync(cancellation).ConfigureAwait(false);
await UnfinishedWordAsync(cancellation).ConfigureAwait(false);
await FedSlowlyAsync(cancellation).ConfigureAwait(false);
await PoliteStopAsync(cancellation).ConfigureAwait(false);
await StopCharacterAsync('\\', cancellation).ConfigureAwait(false);
await StopCharacterAsync('~', cancellation).ConfigureAwait(false);
await SentWholeAsync(cancellation).ConfigureAwait(false);
}
/// Question 11. N1MM hands MMTTY the whole message with `SendString` and
/// calls `SetMmttyPTT(1)` 400 ms later, and MMTTY ends the transmission
/// itself. This program hands the message over one character at a time with
/// `PostMmttyMessage(4, ...)` and the same stop does nothing. Is it the way
/// the text arrives that makes the difference?
private async Task SentWholeAsync(CancellationToken cancellation)
{
log.Step("11. the whole message with SendString, then SetMmttyPTT(1) as N1MM sends it");
TakeReceived();
await KeyAsync(cancellation).ConfigureAwait(false);
await engine.SendAsync(Message, cancellation).ConfigureAwait(false);
log.Write($"pushed {Message.Length} characters in one call");
await Task.Delay(TimeSpan.FromMilliseconds(400), cancellation).ConfigureAwait(false);
await engine.SetPttAsync(false, cancellation).ConfigureAwait(false);
log.Write("SetMmttyPTT(1) sent 400 ms after the push, which is N1MM's wait");
TimeSpan waited = await WatchAsync(TimeSpan.FromSeconds(10), cancellation).ConfigureAwait(false);
log.Write(waited >= TimeSpan.Zero
? $"the transmitter dropped {waited.TotalMilliseconds:0} ms after the stop"
: "the transmitter stayed up");
log.Write($"received: \"{TakeReceived()}\"");
await UnkeyAsync(cancellation).ConfigureAwait(false);
}
/// Question 10. MMTTY's macro language ends a transmission with `\` at the
/// end of a macro, and `~` stops the carrier. The only way into the engine
/// from here is `PostMmttyMessage(4, ...)`, one typed character, so the
/// question is whether a character typed that way is read as a command or
/// transmitted as text. A stop that travels with the text is worth far more
/// than one timed from outside: it lands exactly at the end of the message
/// with nothing held on after it.
private async Task StopCharacterAsync(char candidate, CancellationToken cancellation)
{
log.Step($"10. \"{Word}\" and then '{candidate}' typed as a character");
TakeReceived();
await KeyAsync(cancellation).ConfigureAwait(false);
await TypeAsync(Word, cancellation).ConfigureAwait(false);
await engine.TypeAsync(candidate, cancellation).ConfigureAwait(false);
TimeSpan waited = await WatchAsync(TimeSpan.FromSeconds(5), cancellation).ConfigureAwait(false);
log.Write(waited >= TimeSpan.Zero
? $"'{candidate}' dropped the transmitter after {waited.TotalMilliseconds:0} ms"
: $"'{candidate}' did not drop the transmitter");
log.Write($"received: \"{TakeReceived()}\"");
await engine.ReleaseKeyAsync(cancellation).ConfigureAwait(false);
await StoppedAsync(cancellation).ConfigureAwait(false);
}
/// Question 9. Does `SetMmttyPTT(1)` drop the transmitter at all? The
/// window sends it at the end of every message and the engine went on
/// transmitting, so the abort that follows it a second and a half later is
/// what unkeys, and it cut the last character off a message once. Nothing
/// is aborted here until the question is answered, and if the polite stop
/// does nothing the `PTT` property is put back to false to see whether that
/// does.
private async Task PoliteStopAsync(CancellationToken cancellation)
{
log.Step($"9. \"{Message}\" fed slowly, then SetMmttyPTT(1) and nothing else");
TakeReceived();
await KeyAsync(cancellation).ConfigureAwait(false);
foreach (char character in Message)
{
await engine.TypeAsync(character, cancellation).ConfigureAwait(false);
await Task.Delay(CharacterTime, cancellation).ConfigureAwait(false);
}
await engine.SetPttAsync(false, cancellation).ConfigureAwait(false);
log.Write("SetMmttyPTT(1) sent with the message fed");
TimeSpan waited = await WatchAsync(TimeSpan.FromSeconds(8), cancellation).ConfigureAwait(false);
if (waited >= TimeSpan.Zero)
{
log.Write($"the polite stop dropped the transmitter after {waited.TotalMilliseconds:0} ms");
return;
}
log.Write("the polite stop did not drop the transmitter");
await engine.ReleaseKeyAsync(cancellation).ConfigureAwait(false);
log.Write("PTT property put back to false");
waited = await WatchAsync(TimeSpan.FromSeconds(3), cancellation).ConfigureAwait(false);
log.Write(waited >= TimeSpan.Zero
? $"the property dropped the transmitter after {waited.TotalMilliseconds:0} ms"
: "the property did not drop the transmitter either");
log.Write($"received: \"{TakeReceived()}\"");
await UnkeyAsync(cancellation).ConfigureAwait(false);
}
/// Watches the transmit state and the count until the engine says it has
/// stopped. Returns how long that took, or -1 when it never did.
private async Task<TimeSpan> WatchAsync(TimeSpan patience, CancellationToken cancellation)
{
DateTime from = DateTime.UtcNow;
DateTime giveUp = from + patience;
while (DateTime.UtcNow < giveUp)
{
int left = await AskAsync("", cancellation).ConfigureAwait(false);
if (!engine.IsTransmitting)
{
return DateTime.UtcNow - from;
}
log.Write($"{(DateTime.UtcNow - from).TotalMilliseconds,6:0} ms transmitting, TxBufLen: {Answer(left)}");
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellation).ConfigureAwait(false);
}
return TimeSpan.FromMilliseconds(-1);
}
/// Question 6. The window feeds the engine one character every character
/// time and keeps it nearly empty, so the engine sits with an empty buffer
/// between characters. Does it drop the transmitter there, and does it
/// hold it when the feeding stops altogether? The pane is drawn on the
/// answer: a transmitter that drops by itself is not the end of a message.
private async Task FedSlowlyAsync(CancellationToken cancellation)
{
log.Step($"7. \"{Message}\" fed one character every {CharacterTime.TotalMilliseconds:0} ms");
TakeReceived();
await KeyAsync(cancellation).ConfigureAwait(false);
foreach (char character in Message)
{
await engine.TypeAsync(character, cancellation).ConfigureAwait(false);
await Task.Delay(CharacterTime, cancellation).ConfigureAwait(false);
}
log.Write($"fed {Message.Length} characters; transmitting: {engine.IsTransmitting}");
log.Write("an unkey above this line is the engine dropping between two characters");
log.Step("8. keyed with nothing more to feed");
for (int look = 0; look < 12; look++)
{
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellation).ConfigureAwait(false);
log.Write($"transmitting: {engine.IsTransmitting}, TxBufLen: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
}
log.Write($"received: \"{TakeReceived()}\"");
log.Write("still transmitting after three idle seconds means the engine holds the transmitter itself");
await UnkeyAsync(cancellation).ConfigureAwait(false);
}
/// Question 1. `DISP_E_UNKNOWNNAME` is 0x80020006; a name the control knows