Files
Nonemm/tools/Nonemm.EngineProbe/Program.cs
ericek111 917a05b898 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
2026-09-01 22:51:43 +00:00

124 lines
4.3 KiB
C#

using Nonemm.Digital;
namespace Nonemm.EngineProbe;
/// Starts MMTTY through the bridge, asks it the questions in `EngineProbe`, and
/// writes what it answered to a file. It has to run where MMTTY runs: a sound
/// card and a Wine prefix with XMMT.ocx registered.
public static class Program
{
private const string Usage = """
usage: engine probe --engine <path to MMTTY.EXE> [options]
--engine <path> the engine to start, as a Linux path
--bridge <path> the bridge program (default bridge/nonemm-mmtty-bridge.exe)
--prefix <path> WINEPREFIX to run in (default: Wine's own)
--ptt <port> serial port to key (default: none, so no radio is keyed)
--out <file> where to write the report (default engine-probe.log)
The probe transmits: MMTTY makes tones on the sound card for about half a
minute. It keys no serial port unless --ptt says so, but a rig listening to
that sound card through VOX will still go on the air.
""";
public static async Task<int> Main(string[] arguments)
{
Dictionary<string, string> options;
try
{
options = Read(arguments);
}
catch (ArgumentException problem)
{
Console.Error.WriteLine(problem.Message);
Console.Error.WriteLine();
Console.Error.WriteLine(Usage);
return 1;
}
using CancellationTokenSource stopping = new();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
stopping.Cancel();
};
using ProbeLog log = new(Option(options, "out", "engine-probe.log"));
MmttyEngine engine = new(
new WineBridgeChannel(
Option(options, "bridge", Path.Combine("bridge", "nonemm-mmtty-bridge.exe")),
options.GetValueOrDefault("prefix")),
new MmttyOptions
{
EnginePath = options["engine"],
PttPort = Option(options, "ptt", ""),
});
try
{
log.Write($"starting {options["engine"]}");
await engine.StartAsync(stopping.Token).ConfigureAwait(false);
log.Write($"MMTTY {engine.Version} is up");
await new EngineProbe(engine, log).RunAsync(stopping.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
log.Write("stopped");
}
catch (Exception problem)
{
log.Write($"failed: {problem.Message}");
Console.Error.WriteLine(problem);
return 1;
}
finally
{
await Shutdown(engine, log).ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine($"the report is in {log.Path}");
}
return 0;
}
/// 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.AbortAsync().ConfigureAwait(false);
await engine.StopAsync().ConfigureAwait(false);
}
catch (Exception problem)
{
log.Write($"the engine did not shut down cleanly: {problem.Message}");
}
engine.Dispose();
}
private static Dictionary<string, string> Read(string[] arguments)
{
Dictionary<string, string> options = [];
for (int i = 0; i < arguments.Length; i += 2)
{
if (!arguments[i].StartsWith("--", StringComparison.Ordinal))
{
throw new ArgumentException($"expected an option, found {arguments[i]}");
}
if (i + 1 >= arguments.Length)
{
throw new ArgumentException($"{arguments[i]} needs a value");
}
options[arguments[i][2..]] = arguments[i + 1];
}
if (!options.ContainsKey("engine"))
{
throw new ArgumentException("--engine is required");
}
return options;
}
private static string Option(Dictionary<string, string> options, string name, string fallback) =>
options.TryGetValue(name, out string? given) ? given : fallback;
}