Key MMTTY the way N1MM does, with the control's PTT property
Nothing was ever transmitted. SetMmttyPTT does not start a transmission: N1MM
calls it with 1 to stop once the buffer is empty, which is its XmitOff, and with
0 to stop now, which is its AbortXmit. A transmission starts by setting the
control's PTT property, in XmitOn.
So the bridge learns `key <0|1>` for that property, and MmttyEngine now keys
with it, ends a message with SetMmttyPTT(1) so the buffer still goes out, and
aborts with SetMmttyPTT(0). This is the digital {TX} in the entry window and the
digital window as well as the probe: none of them could key the engine before.
The probe checks that the engine keys before it measures anything, and stops
with a plain statement if it does not, rather than reporting numbers from an
engine sitting still. It also asks a new question: whether the engine holds a
word until the space after it, which is MMTTY's Way to send. Received characters
are marked as noise while the engine is not transmitting, since a machine with a
sound card decodes the band all the way through the run.
The first run on a real engine says MMTTY 1.70 connects, the control answers
TxBufLen and refuses NotAProperty with DISP_E_UNKNOWNNAME. What TxBufLen counts
is still open: read while nothing was transmitting it rose over time and rose by
four after four backspaces, which is not what characters-left would do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
This commit is contained in:
@@ -3,19 +3,22 @@ using Nonemm.Digital;
|
||||
|
||||
namespace Nonemm.EngineProbe;
|
||||
|
||||
/// Four questions about MMTTY that cannot be answered without MMTTY running,
|
||||
/// asked in order and written to the report:
|
||||
/// Questions about MMTTY that cannot be answered without MMTTY running, asked
|
||||
/// in order and written to the report:
|
||||
///
|
||||
/// 1. Does the control answer `TxBufLen`, and is a name it does not know
|
||||
/// distinguishable from one it does?
|
||||
/// 2. While a message is going out, does that number count down at the
|
||||
/// character rate? If it does, it says exactly how much of the message is
|
||||
/// still in the engine and can still be taken back.
|
||||
/// 3. Does a backspace pushed in as a character delete a character 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. What comes back on the receive side while transmitting, and when? With
|
||||
/// 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.
|
||||
/// 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.
|
||||
///
|
||||
@@ -32,27 +35,34 @@ public sealed class EngineProbe
|
||||
/// of one.
|
||||
private static readonly TimeSpan Patience = TimeSpan.FromSeconds(30);
|
||||
|
||||
private const string Message = "CQ TEST DE OM5M OM5M";
|
||||
private const string Alphabet = "ABCDEFGHIJKLMNOP";
|
||||
/// A count that has not moved for this long is not going to move.
|
||||
private static readonly TimeSpan Still = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// How long the engine is given to key up and to drop again.
|
||||
private static readonly TimeSpan Keying = TimeSpan.FromSeconds(3);
|
||||
|
||||
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 string Short = "MNOPQRST ";
|
||||
private const int TooManyBackspaces = 6;
|
||||
private const string Afterwards = "XX";
|
||||
private const string Afterwards = "XX ";
|
||||
|
||||
private readonly MmttyEngine engine;
|
||||
private readonly ProbeLog log;
|
||||
private readonly StringBuilder received = new();
|
||||
private readonly Lock gate = new();
|
||||
private TaskCompletionSource<int>? asking;
|
||||
private TaskCompletionSource<bool>? keying;
|
||||
|
||||
public EngineProbe(MmttyEngine engine, ProbeLog log)
|
||||
{
|
||||
this.engine = engine;
|
||||
this.log = log;
|
||||
engine.Reported += (_, what) => log.Write($"bridge: {what}");
|
||||
engine.TransmitChanged += (_, on) => log.Write(on ? "engine keyed" : "engine unkeyed");
|
||||
engine.TransmitChanged += WhenTransmitChanged;
|
||||
engine.Buffered += WhenBuffered;
|
||||
engine.Received += WhenReceived;
|
||||
}
|
||||
@@ -61,7 +71,12 @@ public sealed class EngineProbe
|
||||
{
|
||||
await NamesAsync(cancellation).ConfigureAwait(false);
|
||||
await IdleAsync(cancellation).ConfigureAwait(false);
|
||||
if (!await KeyingAsync(cancellation).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await MessageAsync(cancellation).ConfigureAwait(false);
|
||||
await WordAsync(cancellation).ConfigureAwait(false);
|
||||
await BackspaceAsync(cancellation).ConfigureAwait(false);
|
||||
await TooLateAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
@@ -88,55 +103,86 @@ public sealed class EngineProbe
|
||||
}
|
||||
}
|
||||
|
||||
/// Questions 2 and 4. The whole message is pushed as fast as the bridge
|
||||
/// Nothing else in the report means anything until the engine keys, so this
|
||||
/// stops the run rather than letting the rest measure an engine that is
|
||||
/// sitting still.
|
||||
private async Task<bool> KeyingAsync(CancellationToken cancellation)
|
||||
{
|
||||
log.Step("3. does the engine key up");
|
||||
if (!await KeyAsync(cancellation).ConfigureAwait(false))
|
||||
{
|
||||
log.Write("the engine did not key: nothing below this would mean anything, so stopping");
|
||||
log.Write("keying is the control's PTT property; SetMmttyPTT only stops a transmission");
|
||||
return false;
|
||||
}
|
||||
log.Write($"TxBufLen while keyed and idle: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
await UnkeyAsync(cancellation).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Questions 2 and 5. The whole message is pushed as fast as the bridge
|
||||
/// takes it, so what the count does afterwards is the engine transmitting
|
||||
/// rather than the probe feeding.
|
||||
private async Task MessageAsync(CancellationToken cancellation)
|
||||
{
|
||||
log.Step($"3. \"{Message}\" pushed in one go, then TxBufLen every {PollInterval.TotalMilliseconds} ms");
|
||||
log.Step($"4. \"{Message}\" pushed in one go, then TxBufLen every {PollInterval.TotalMilliseconds} ms");
|
||||
TakeReceived();
|
||||
await engine.SetPttAsync(true, cancellation).ConfigureAwait(false);
|
||||
await KeyAsync(cancellation).ConfigureAwait(false);
|
||||
await TypeAsync(Message, cancellation).ConfigureAwait(false);
|
||||
log.Write($"pushed {Message.Length} characters");
|
||||
await DrainAsync(cancellation).ConfigureAwait(false);
|
||||
log.Write($"received while transmitting: \"{TakeReceived()}\"");
|
||||
await engine.SetPttAsync(false, cancellation).ConfigureAwait(false);
|
||||
await Task.Delay(PollInterval, cancellation).ConfigureAwait(false);
|
||||
await UnkeyAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// Question 3. The wait is there so the engine is partway through the
|
||||
/// alphabet when the backspaces arrive: the ones over transmitted
|
||||
/// characters are the ones MMTTY has to refuse.
|
||||
/// Question 3. A word with no space after it is what MMTTY holds back when
|
||||
/// it is set to Word out, and that changes what "still in the buffer"
|
||||
/// means.
|
||||
private async Task WordAsync(CancellationToken cancellation)
|
||||
{
|
||||
log.Step($"5. \"{Word}\" with no space after it, then the space");
|
||||
TakeReceived();
|
||||
await KeyAsync(cancellation).ConfigureAwait(false);
|
||||
await TypeAsync(Word, cancellation).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen three seconds after the word: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
log.Write($"received so far: \"{TakeReceived()}\"");
|
||||
log.Write("nothing received here means the engine is set to Word out and is holding it");
|
||||
await TypeAsync(" ", cancellation).ConfigureAwait(false);
|
||||
await DrainAsync(cancellation).ConfigureAwait(false);
|
||||
log.Write($"received after the space: \"{TakeReceived()}\"");
|
||||
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)
|
||||
{
|
||||
log.Step($"4. \"{Alphabet}\", then {Backspaces} backspaces once it is under way");
|
||||
log.Step($"6. \"{Alphabet}\", then {Backspaces} backspaces once it is under way");
|
||||
TakeReceived();
|
||||
await engine.SetPttAsync(true, cancellation).ConfigureAwait(false);
|
||||
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);
|
||||
int before = await AskAsync("", cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen before the backspaces: {Answer(before)}");
|
||||
log.Write($"TxBufLen before the backspaces: {Answer(await AskAsync("", cancellation).ConfigureAwait(false))}");
|
||||
await TypeAsync(new string(Backspace, Backspaces), cancellation).ConfigureAwait(false);
|
||||
int after = await AskAsync("", cancellation).ConfigureAwait(false);
|
||||
log.Write($"TxBufLen after the backspaces: {Answer(after)}");
|
||||
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);
|
||||
string went = TakeReceived();
|
||||
log.Write($"received while transmitting: \"{went}\"");
|
||||
log.Write($"the last {Backspaces} letters of \"{Alphabet}\" are the ones that should be missing");
|
||||
await engine.SetPttAsync(false, 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 3: more backspaces than there are characters
|
||||
/// 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($"5. \"{Short}\", then {TooManyBackspaces} backspaces near the end of it");
|
||||
log.Step($"7. \"{Short}\", then {TooManyBackspaces} backspaces near the end of it");
|
||||
TakeReceived();
|
||||
await engine.SetPttAsync(true, cancellation).ConfigureAwait(false);
|
||||
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)}");
|
||||
@@ -145,39 +191,16 @@ public sealed class EngineProbe
|
||||
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}\" once, not twice");
|
||||
await engine.SetPttAsync(false, cancellation).ConfigureAwait(false);
|
||||
log.Write($"what went out should end in \"{Afterwards.Trim()}\" once, not twice");
|
||||
await UnkeyAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// Waits until the engine holds no more than `wanted` characters, so the
|
||||
/// next thing the probe does lands at a known point in the message.
|
||||
private async Task<int> WaitForAsync(int wanted, CancellationToken cancellation)
|
||||
{
|
||||
DateTime giveUp = DateTime.UtcNow + Patience;
|
||||
while (DateTime.UtcNow < giveUp)
|
||||
{
|
||||
int left = await AskAsync("", cancellation).ConfigureAwait(false);
|
||||
if (left < 0)
|
||||
{
|
||||
log.Write($"no answer to wait on, guessing at {Short.Length - wanted} characters gone");
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellation).ConfigureAwait(false);
|
||||
return left;
|
||||
}
|
||||
if (left <= wanted)
|
||||
{
|
||||
return left;
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// Polls until the engine says it has nothing left, or until the patience
|
||||
/// runs out. A control that does not answer has nothing to poll, so the
|
||||
/// wait is the whole of a message instead.
|
||||
/// Polls until the engine says it has nothing left, until the count stops
|
||||
/// moving, or until the patience runs out.
|
||||
private async Task DrainAsync(CancellationToken cancellation)
|
||||
{
|
||||
DateTime giveUp = DateTime.UtcNow + Patience;
|
||||
DateTime moved = DateTime.UtcNow;
|
||||
int last = int.MinValue;
|
||||
while (DateTime.UtcNow < giveUp)
|
||||
{
|
||||
@@ -186,15 +209,15 @@ public sealed class EngineProbe
|
||||
{
|
||||
log.Write($"TxBufLen: {Answer(left)}");
|
||||
last = left;
|
||||
moved = DateTime.UtcNow;
|
||||
}
|
||||
if (left == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (left < 0)
|
||||
if (DateTime.UtcNow - moved > Still)
|
||||
{
|
||||
log.Write("no answer to poll, waiting out the message instead");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation).ConfigureAwait(false);
|
||||
log.Write($"the count has not moved for {Still.TotalSeconds} s, so it is not counting down");
|
||||
return;
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation).ConfigureAwait(false);
|
||||
@@ -202,6 +225,67 @@ public sealed class EngineProbe
|
||||
log.Write($"gave up waiting after {Patience.TotalSeconds} s");
|
||||
}
|
||||
|
||||
/// Waits until the engine holds no more than `wanted` characters, so the
|
||||
/// next thing the probe does lands at a known point in the message.
|
||||
private async Task<int> WaitForAsync(int wanted, CancellationToken cancellation)
|
||||
{
|
||||
DateTime giveUp = DateTime.UtcNow + Still;
|
||||
while (DateTime.UtcNow < giveUp)
|
||||
{
|
||||
int left = await AskAsync("", cancellation).ConfigureAwait(false);
|
||||
if (left <= wanted)
|
||||
{
|
||||
return left;
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
log.Write($"the count never came down to {wanted}, so the backspaces go in wherever it is");
|
||||
return await AskAsync("", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// N1MM's `{TX}`: the control's PTT property, then wait for the engine to
|
||||
/// say it is transmitting.
|
||||
private async Task<bool> KeyAsync(CancellationToken cancellation)
|
||||
{
|
||||
if (engine.IsTransmitting)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
TaskCompletionSource<bool> keyed = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
lock (gate)
|
||||
{
|
||||
keying = keyed;
|
||||
}
|
||||
await engine.SetPttAsync(true, cancellation).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await keyed.Task.WaitAsync(Keying, cancellation).ConfigureAwait(false);
|
||||
log.Write("keyed");
|
||||
return true;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
log.Write($"no transmit report {Keying.TotalSeconds} s after keying");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// N1MM's `{RX}`: stop once the buffer is empty.
|
||||
private async Task UnkeyAsync(CancellationToken cancellation)
|
||||
{
|
||||
await engine.SetPttAsync(false, cancellation).ConfigureAwait(false);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TypeAsync(string text, CancellationToken cancellation)
|
||||
{
|
||||
foreach (char character in text)
|
||||
@@ -221,7 +305,7 @@ public sealed class EngineProbe
|
||||
asking = answer;
|
||||
}
|
||||
await engine.AskBufferedAsync(property, cancellation).ConfigureAwait(false);
|
||||
return await answer.Task.WaitAsync(Patience, cancellation).ConfigureAwait(false);
|
||||
return await answer.Task.WaitAsync(Keying, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string Answer(int left) => left < 0 ? "no answer" : left.ToString();
|
||||
@@ -234,11 +318,31 @@ public sealed class EngineProbe
|
||||
}
|
||||
}
|
||||
|
||||
private void WhenTransmitChanged(object? sender, bool transmitting)
|
||||
{
|
||||
log.Write(transmitting ? "engine keyed" : "engine unkeyed");
|
||||
if (!transmitting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (gate)
|
||||
{
|
||||
keying?.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// While the engine is not transmitting this is the demodulator hearing
|
||||
/// whatever is on the band, which is noise on a quiet frequency. Only what
|
||||
/// arrives while transmitting is about the transmission.
|
||||
private void WhenReceived(object? sender, string text)
|
||||
{
|
||||
foreach (char character in text)
|
||||
{
|
||||
log.Write($"rx {Printable(character)}");
|
||||
log.Write($"{(engine.IsTransmitting ? "rx" : "noise")} {Printable(character)}");
|
||||
}
|
||||
if (!engine.IsTransmitting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (received)
|
||||
{
|
||||
|
||||
@@ -79,13 +79,14 @@ public static class Program
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// The engine is left keyed if the probe stopped partway through, so PTT is
|
||||
/// dropped before the engine is, whatever happened.
|
||||
/// The engine is left keyed if the probe stopped partway through, so the
|
||||
/// transmission is aborted before the engine is shut down, whatever
|
||||
/// happened.
|
||||
private static async Task Shutdown(MmttyEngine engine, ProbeLog log)
|
||||
{
|
||||
try
|
||||
{
|
||||
await engine.SetPttAsync(false).ConfigureAwait(false);
|
||||
await engine.AbortAsync().ConfigureAwait(false);
|
||||
await engine.StopAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception problem)
|
||||
|
||||
Reference in New Issue
Block a user