Pace the pump on what MMTTY says it has left
The engine probe answered two questions on the air, and they point opposite
ways.
TxBufLen is the number of characters left to transmit. Seventeen characters
pushed, five decoded back, and it read 12, counting down to 0 as the message
went out. So the pump no longer counts character times off the clock: it asks
the engine, feeds while the answer is under Lead, and asks again. The baud rate
is now only the fallback for an engine that will not answer.
A backspace is not an edit. Pushed in as a character it made the count go up by
four and the text went out unchanged, so text the engine has been given cannot
be taken back, only aborted. EngineTypeAhead was built on the opposite belief
and is gone, along with the setting that chose it and the interface that existed
to switch between the two.
A count that stops going down means the engine is holding what it has: MMTTY set
to Word out keeps a word until the space after it. The pump feeds one character
per look while that lasts, so the space arrives and the word goes out rather
than the message sitting there.
The probe kept two bugs of its own that this run showed: it read the count a
millisecond after pushing, saw 0 and called the message finished, and it started
a step while the previous unkey was still in flight, so that step ran with the
engine down. It now needs two empty answers in a row and waits for the engine to
say it has stopped. It also asks what {RX} does to a word the engine is holding,
which decides whether a macro without a trailing space loses its last word.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
This commit is contained in:
@@ -14,14 +14,17 @@ namespace Nonemm.EngineProbe;
|
||||
/// 3. Does the engine hold a word until the space after it? That is MMTTY's
|
||||
/// Option ▸ Way to send, and Word out is what its help calls the usual
|
||||
/// setting.
|
||||
/// 4. Does a backspace pushed in as a character delete one the engine has not
|
||||
/// transmitted yet, and what happens to backspaces over characters that have
|
||||
/// already gone? Those have to be refused: text typed after them would
|
||||
/// otherwise go out twice.
|
||||
/// 4. Does `{RX}` — `SetMmttyPTT(1)`, stop once the buffer is empty — send a
|
||||
/// word the engine is holding, or drop it? A macro whose last word has no
|
||||
/// space after it hangs on the answer.
|
||||
/// 5. What comes back on the receive side while transmitting, and when? With
|
||||
/// the sound loopback off that is MMTTY echoing its transmit window; with it
|
||||
/// on it is the demodulator hearing the transmission.
|
||||
///
|
||||
/// A backspace is not on the list any more: pushed in as a character, MMTTY
|
||||
/// counted it as one more character to transmit and sent the text unchanged.
|
||||
/// It is not an edit.
|
||||
///
|
||||
/// The answers decide whether the transmit buffer can be handed to MMTTY
|
||||
/// instead of being paced from here. `docs/unfinished.md` states what each one
|
||||
/// means.
|
||||
@@ -43,12 +46,6 @@ public sealed class EngineProbe
|
||||
|
||||
private const string Message = "CQ TEST DE OM5M OM5M ";
|
||||
private const string Word = "ABCD";
|
||||
private const string Alphabet = "ABCDEFGHIJKLMNOP ";
|
||||
private const int Backspaces = 4;
|
||||
private const char Backspace = '\b';
|
||||
private const string Short = "MNOPQRST ";
|
||||
private const int TooManyBackspaces = 6;
|
||||
private const string Afterwards = "XX ";
|
||||
|
||||
private readonly MmttyEngine engine;
|
||||
private readonly ProbeLog log;
|
||||
@@ -77,8 +74,7 @@ public sealed class EngineProbe
|
||||
}
|
||||
await MessageAsync(cancellation).ConfigureAwait(false);
|
||||
await WordAsync(cancellation).ConfigureAwait(false);
|
||||
await BackspaceAsync(cancellation).ConfigureAwait(false);
|
||||
await TooLateAsync(cancellation).ConfigureAwait(false);
|
||||
await UnfinishedWordAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// Question 1. `DISP_E_UNKNOWNNAME` is 0x80020006; a name the control knows
|
||||
@@ -154,54 +150,33 @@ public sealed class EngineProbe
|
||||
await UnkeyAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// Question 4. The wait is there so the engine is partway through the
|
||||
/// alphabet when the backspaces arrive.
|
||||
private async Task BackspaceAsync(CancellationToken cancellation)
|
||||
/// Question 4. The word has no space after it, so an engine set to Word
|
||||
/// out is still holding it when the transmission is told to stop.
|
||||
private async Task UnfinishedWordAsync(CancellationToken cancellation)
|
||||
{
|
||||
log.Step($"6. \"{Alphabet}\", then {Backspaces} backspaces once it is under way");
|
||||
log.Step($"6. \"{Word}\" with no space after it, then {{RX}}");
|
||||
TakeReceived();
|
||||
await KeyAsync(cancellation).ConfigureAwait(false);
|
||||
await TypeAsync(Alphabet, cancellation).ConfigureAwait(false);
|
||||
log.Write($"pushed {Alphabet.Length} characters");
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen before the backspaces: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
await TypeAsync(new string(Backspace, Backspaces), cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen after the backspaces: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
log.Write($"a drop of {Backspaces} means the engine took them out of its buffer");
|
||||
await DrainAsync(cancellation).ConfigureAwait(false);
|
||||
log.Write($"received while transmitting: \"{TakeReceived()}\"");
|
||||
log.Write($"the last {Backspaces} letters of \"{Alphabet.Trim()}\" are the ones that should be missing");
|
||||
await UnkeyAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// The other half of question 4: more backspaces than there are characters
|
||||
/// left to take back. If the extra ones are ignored, what goes out is the
|
||||
/// transmitted part followed by the new text. If they do something else,
|
||||
/// this is where it shows.
|
||||
private async Task TooLateAsync(CancellationToken cancellation)
|
||||
{
|
||||
log.Step($"7. \"{Short}\", then {TooManyBackspaces} backspaces near the end of it");
|
||||
TakeReceived();
|
||||
await KeyAsync(cancellation).ConfigureAwait(false);
|
||||
await TypeAsync(Short, cancellation).ConfigureAwait(false);
|
||||
int left = await WaitForAsync(2, cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen when the backspaces go in: {Answer(left)}");
|
||||
await TypeAsync(new string(Backspace, TooManyBackspaces), cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen after them: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
await TypeAsync(Afterwards, cancellation).ConfigureAwait(false);
|
||||
await DrainAsync(cancellation).ConfigureAwait(false);
|
||||
log.Write($"received while transmitting: \"{TakeReceived()}\"");
|
||||
log.Write($"what went out should end in \"{Afterwards.Trim()}\" once, not twice");
|
||||
await TypeAsync(Word, cancellation).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen before {{RX}}: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
await UnkeyAsync(cancellation).ConfigureAwait(false);
|
||||
log.Write($"received: \"{TakeReceived()}\"");
|
||||
log.Write($"\"{Word}\" here means SetMmttyPTT(1) sends a held word before it stops");
|
||||
}
|
||||
|
||||
/// Polls until the engine says it has nothing left, until the count stops
|
||||
/// moving, or until the patience runs out.
|
||||
///
|
||||
/// The engine counts what it has been given a moment after it is given it,
|
||||
/// so a single empty answer straight after a push means the push has not
|
||||
/// registered, not that the message has gone.
|
||||
private async Task DrainAsync(CancellationToken cancellation)
|
||||
{
|
||||
DateTime giveUp = DateTime.UtcNow + Patience;
|
||||
DateTime moved = DateTime.UtcNow;
|
||||
int last = int.MinValue;
|
||||
bool wasEmpty = false;
|
||||
while (DateTime.UtcNow < giveUp)
|
||||
{
|
||||
int left = await AskAsync("", cancellation).ConfigureAwait(false);
|
||||
@@ -211,10 +186,11 @@ public sealed class EngineProbe
|
||||
last = left;
|
||||
moved = DateTime.UtcNow;
|
||||
}
|
||||
if (left == 0)
|
||||
if (left == 0 && wasEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
wasEmpty = left == 0;
|
||||
if (DateTime.UtcNow - moved > Still)
|
||||
{
|
||||
log.Write($"the count has not moved for {Still.TotalSeconds} s, so it is not counting down");
|
||||
@@ -270,20 +246,32 @@ public sealed class EngineProbe
|
||||
}
|
||||
}
|
||||
|
||||
/// N1MM's `{RX}`: stop once the buffer is empty.
|
||||
/// N1MM's `{RX}`: stop once the buffer is empty. Waits for the engine to
|
||||
/// say it has stopped, because the next step keys again and would otherwise
|
||||
/// see the old state and skip it.
|
||||
private async Task UnkeyAsync(CancellationToken cancellation)
|
||||
{
|
||||
await engine.SetPttAsync(false, cancellation).ConfigureAwait(false);
|
||||
if (await StoppedAsync(cancellation).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
log.Write("still transmitting, stopping it the hard way");
|
||||
await engine.AbortAsync(cancellation).ConfigureAwait(false);
|
||||
if (!await StoppedAsync(cancellation).ConfigureAwait(false))
|
||||
{
|
||||
log.Write("the engine is still reporting a transmission after the abort");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> StoppedAsync(CancellationToken cancellation)
|
||||
{
|
||||
DateTime giveUp = DateTime.UtcNow + Keying;
|
||||
while (engine.IsTransmitting && DateTime.UtcNow < giveUp)
|
||||
{
|
||||
await Task.Delay(PollInterval, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
if (engine.IsTransmitting)
|
||||
{
|
||||
log.Write("still transmitting, stopping it the hard way");
|
||||
await engine.AbortAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
return !engine.IsTransmitting;
|
||||
}
|
||||
|
||||
private async Task TypeAsync(string text, CancellationToken cancellation)
|
||||
|
||||
Reference in New Issue
Block a user