MMTTY has a type-ahead buffer of its own: characters go into it, a backspace takes back one it has not transmitted, and TxBufLen says how many are left. Used that way it paces itself, so there is no gap between characters to tune and no baud rate to keep in step with the engine. EngineTypeAhead does that, and Config > Digital picks between it and the pump that is there now. Off is still the default: three things it rests on have never been seen with a real engine. tools/Nonemm.EngineProbe asks the engine those three questions and writes the answers to a file. It starts MMTTY through the bridge, pushes a message, polls TxBufLen while it goes out, backspaces over text that has and has not been transmitted, and logs what came back on the receive side and when. The bridge learns one verb for it: `buffer` reads TxBufLen and answers with the count, or -1 when the control will not say. A property the control does not know is a log line rather than an error, since it stops nothing. TypeAhead and EngineTypeAhead share the TransmitBuffer interface, which is what the digital window now works through, so the window does not know which one it has. docs/unfinished.md states what each buffer assumes and how to run the probe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
240 lines
6.6 KiB
C#
240 lines
6.6 KiB
C#
using System.Text;
|
|
using Nonemm.Digital;
|
|
|
|
namespace Nonemm.Digital.Tests;
|
|
|
|
/// The text waiting to go out, fed to the engine a few characters at a time.
|
|
/// The pump is run at a baud rate no radio uses so the tests do not wait for
|
|
/// RTTY.
|
|
public class TypeAheadTests
|
|
{
|
|
private static readonly TimeSpan Patience = TimeSpan.FromSeconds(5);
|
|
|
|
/// Fast enough that a message goes out in milliseconds, slow enough that a
|
|
/// test can still catch the pump partway through.
|
|
private const double Fast = 7500;
|
|
|
|
/// A character time of 100 ms, so a test can tell the characters sent ahead
|
|
/// from the ones that wait for the engine.
|
|
private const double Slow = 75;
|
|
|
|
private readonly StringBuilder went = new();
|
|
|
|
private string Sent
|
|
{
|
|
get
|
|
{
|
|
lock (went)
|
|
{
|
|
return went.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
private TypeAhead Buffer(double baud = Fast) =>
|
|
new(
|
|
(character, _) =>
|
|
{
|
|
lock (went)
|
|
{
|
|
went.Append(character);
|
|
}
|
|
return Task.CompletedTask;
|
|
},
|
|
baud);
|
|
|
|
private static async Task WaitForAsync(Func<bool> ready)
|
|
{
|
|
DateTime giveUp = DateTime.UtcNow + Patience;
|
|
while (!ready() && DateTime.UtcNow < giveUp)
|
|
{
|
|
await Task.Delay(2);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AMessageGoesOut()
|
|
{
|
|
using TypeAhead buffer = Buffer();
|
|
|
|
buffer.Append("CQ TEST");
|
|
|
|
await WaitForAsync(() => Sent == "CQ TEST");
|
|
Assert.Equal("CQ TEST", Sent);
|
|
Assert.Equal("CQ TEST", buffer.Sent);
|
|
Assert.Equal("", buffer.Pending);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WhatHasGoneOutIsNotPendingAnyMore()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: 40);
|
|
|
|
buffer.Append("CQ TEST DE OM5M");
|
|
|
|
await WaitForAsync(() => Sent.Length >= 2);
|
|
Assert.StartsWith(buffer.Sent, "CQ TEST DE OM5M", StringComparison.Ordinal);
|
|
Assert.Equal("CQ TEST DE OM5M", buffer.Sent + buffer.Pending);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheOperatorRewritesWhatHasNotGoneOut()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: 60);
|
|
buffer.Append("OM5X 599 001");
|
|
|
|
await WaitForAsync(() => buffer.Sent.Length >= 5);
|
|
buffer.Rewrite("599 002", TransmitBuffer.NoCursor);
|
|
|
|
await WaitForAsync(() => buffer.Pending.Length == 0);
|
|
Assert.EndsWith("599 002", Sent, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("001", Sent, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NothingGoesOutFromBehindTheCursor()
|
|
{
|
|
using TypeAhead buffer = Buffer();
|
|
|
|
buffer.Rewrite("CQ TEST", 2);
|
|
|
|
await WaitForAsync(() => Sent.Length == 2);
|
|
await Task.Delay(50);
|
|
Assert.Equal("CQ", Sent);
|
|
Assert.Equal(" TEST", buffer.Pending);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheRestGoesOutWhenTheCursorMovesOn()
|
|
{
|
|
using TypeAhead buffer = Buffer();
|
|
buffer.Rewrite("CQ TEST", 2);
|
|
await WaitForAsync(() => Sent.Length == 2);
|
|
|
|
buffer.Cursor = TransmitBuffer.NoCursor;
|
|
|
|
await WaitForAsync(() => Sent == "CQ TEST");
|
|
Assert.Equal("CQ TEST", Sent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TextAddedBehindTheCursorStillGoesOut()
|
|
{
|
|
using TypeAhead buffer = Buffer();
|
|
buffer.Rewrite("CQ", 2);
|
|
await WaitForAsync(() => Sent == "CQ");
|
|
|
|
buffer.Append(" TEST");
|
|
|
|
await WaitForAsync(() => Sent == "CQ TEST");
|
|
Assert.Equal("CQ TEST", Sent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheBufferSaysWhenTheMessageHasGoneOut()
|
|
{
|
|
using TypeAhead buffer = Buffer();
|
|
int drained = 0;
|
|
buffer.Drained += (_, _) => Interlocked.Increment(ref drained);
|
|
|
|
buffer.Append("TU");
|
|
|
|
await WaitForAsync(() => Volatile.Read(ref drained) == 1);
|
|
Assert.Equal(1, Volatile.Read(ref drained));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AMessageAddedWhileOneIsGoingOutFollowsIt()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: 400);
|
|
buffer.Append("CQ ");
|
|
|
|
buffer.Append("DE OM5M");
|
|
|
|
await WaitForAsync(() => buffer.Pending.Length == 0);
|
|
Assert.Equal("CQ DE OM5M", Sent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DroppingLeavesWhatHasAlreadyGone()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: 60);
|
|
buffer.Append("CQ TEST DE OM5M");
|
|
await WaitForAsync(() => buffer.Sent.Length >= 3);
|
|
|
|
buffer.Drop();
|
|
|
|
string gone = Sent;
|
|
await Task.Delay(60);
|
|
Assert.Equal(gone, Sent);
|
|
Assert.Equal("", buffer.Pending);
|
|
Assert.Equal(gone, buffer.Sent);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClearingEmptiesBothHalves()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: 1);
|
|
buffer.Append("CQ TEST");
|
|
|
|
buffer.Clear();
|
|
|
|
Assert.Equal("", buffer.Pending);
|
|
Assert.Equal("", buffer.Sent);
|
|
}
|
|
|
|
/// The engine is given the second character before it has transmitted the
|
|
/// first, so it has one in hand when the first is done. An engine left with
|
|
/// an empty buffer transmits idle instead, and that idle is added to how
|
|
/// long the message takes.
|
|
[Fact]
|
|
public async Task TheEngineIsKeptOneCharacterAhead()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: Slow);
|
|
|
|
buffer.Append("CQ TEST");
|
|
|
|
await WaitForAsync(() => Sent.Length >= 2);
|
|
Assert.Equal("CQ", Sent);
|
|
}
|
|
|
|
/// Only the lead goes out ahead. The rest waits, which is what leaves it
|
|
/// where the operator can still change it.
|
|
[Fact]
|
|
public async Task NoMoreThanTheLeadGoesToTheEngineAtOnce()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: Slow);
|
|
|
|
buffer.Append("CQ TEST");
|
|
|
|
await WaitForAsync(() => Sent.Length >= 2);
|
|
await Task.Delay(20);
|
|
Assert.Equal(2, Sent.Length);
|
|
}
|
|
|
|
/// The estimate of what the engine still holds is only an estimate. An
|
|
/// engine that says it has stopped transmitting has an empty buffer, and
|
|
/// the next character goes to it at once rather than a character time
|
|
/// later.
|
|
[Fact]
|
|
public async Task AnIdleEngineIsFedWithoutWaiting()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: Slow);
|
|
buffer.Append("CQ TEST");
|
|
await WaitForAsync(() => Sent.Length >= 2);
|
|
|
|
buffer.EngineIdle();
|
|
|
|
await WaitForAsync(() => Sent.Length >= 4);
|
|
Assert.Equal("CQ T", Sent);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACharacterTakesAsLongAsTheBaudRateSays()
|
|
{
|
|
using TypeAhead buffer = Buffer(baud: TypeAhead.DefaultBaud);
|
|
|
|
Assert.Equal(165, buffer.CharacterTime.TotalMilliseconds, 0.5);
|
|
}
|
|
}
|