Read a QTC series out on RTTY

N1MM's Send All: the whole series as one message — the heading, every
line with the operator's spacing between them, then the ending, with
`{TX}` and `{RX}` around it so it keys the transmitter and drops it
again. The shape is N1MM's, from `QTCWindow.cs:3609`. A line is the three
fields joined with hyphens, which is how N1MM writes them, and Snd n
sends one line again for a station that missed it.

One message rather than a button per line because a series is close to a
minute of transmission at 45 baud: the type-ahead buffer holds it and
paces the engine.

Three settings with N1MM's names and defaults — `WAESendAllHeadingText`,
`WAESendAllEndingText` and `WAESQTCSpacing` — on the QTC window's setup
dialog. `{ENTERLF}` is new to the expander and stands for a carriage
return and a line feed; `{QTC}` is filled in by the window, which is the
only place that knows which series is going out.

The messages are tested as text. Nothing has gone on the air.

Left out: N1MM's four SSB recordings, which need a voice keyer, and its
RTTY messages for the station taking traffic down, which are typed into
the digital window's transmit pane here.

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 15:02:08 +00:00
parent 0fc8d7e405
commit 9f635550bd
10 changed files with 255 additions and 33 deletions

View File

@@ -19,8 +19,10 @@ namespace Nonemm.App.Windows;
/// the log and cannot be edited — what is being reported is what was worked.
///
/// On CW it puts the traffic on the air through the entry window's keyer, the
/// way N1MM's QTC window sends through its own entry window. On SSB and RTTY
/// nothing is sent: one needs a voice keyer and the other a digital window.
/// way N1MM's QTC window sends through its own entry window. On RTTY it sends
/// through the digital engine the same way, but a line at a time rather than a
/// field at a time, and Send All reads the whole series out in one message. On
/// SSB nothing is sent: that needs a voice keyer, and there is none here.
public sealed partial class QtcWindow : Window
{
private static IBrush Saved => Themes.Brush(Themes.Current.GoodBackground);
@@ -83,7 +85,9 @@ public sealed partial class QtcWindow : Window
if (!IsCw)
{
return isSending
? $"{traffic.Remaining(station)} of the ten QTCs for {station.Text} are still free"
? IsRtty
? "Send All reads the whole series out — Snd n sends one QTC again"
: $"{traffic.Remaining(station)} of the ten QTCs for {station.Text} are still free"
: "Type the header, then a line per QTC: time, callsign, serial number.";
}
return isSending
@@ -91,10 +95,17 @@ public sealed partial class QtcWindow : Window
: "Shift 1 = ask time Shift 2 = ask call Shift 3 = ask serial";
}
/// Traffic goes out on CW alone. The contest says which mode it is, not
/// what the radio happens to be on, which is how N1MM decides too.
/// Which mode the traffic goes out on. The contest says, not what the radio
/// happens to be on, which is how N1MM decides too.
private bool IsCw => position.Contest.Modes is [ModeCategory.Cw];
private bool IsRtty => position.Contest.Modes.Contains(ModeCategory.Digital);
/// The header of the series as it goes out in a message, which is N1MM's
/// `QTC 3/10`. The box shows the station callsign after it on RTTY, and
/// that is for the operator rather than for the air.
private string SeriesHeader => ready.Count > 0 ? $"QTC {ready[0].SeriesText}" : "";
private void SetDirection(bool sending)
{
if (sending == isSending)
@@ -118,6 +129,9 @@ public sealed partial class QtcWindow : Window
HeaderAgainButton.Content = isSending && IsCw ? "Snd Hdr" : "Hdr Agn";
HeaderAgainButton.IsVisible = !isSending || IsCw;
HeaderCfmButton.IsVisible = !isSending;
// N1MM's Send All, which reads a whole series out in one message. It
// needs the engine to hold the text, so it is RTTY only
SendAllButton.IsVisible = isSending && IsRtty;
ClearButton.IsVisible = !isSending;
CloseButton.Content = IsCw ? "Exit" : "Close";
HeaderBox.IsReadOnly = isSending;
@@ -144,7 +158,7 @@ public sealed partial class QtcWindow : Window
TextBox number = Box(at, 2);
Button again = new()
{
Content = isSending && IsCw ? $"Snd{at + 1}" : $"Agn{at + 1}",
Content = isSending && (IsCw || IsRtty) ? $"Snd{at + 1}" : $"Agn{at + 1}",
Margin = new Avalonia.Thickness(2, 1, 2, 1),
};
Button confirm = new() { Content = $"Cfm{at + 1}", Margin = new Avalonia.Thickness(2, 1, 2, 1) };
@@ -195,13 +209,11 @@ public sealed partial class QtcWindow : Window
return;
}
ready.AddRange(traffic.ToSend(station, session.Settings.QtcLinesPerSeries));
// the contest is what makes it RTTY, not whatever the radio is on now
bool rtty = position.Contest.Modes.Contains(ModeCategory.Digital);
HeaderBox.Text = ready.Count == 0
? ""
: rtty
? $"QTC {ready[0].SeriesText} - {station.Text}"
: $"QTC {ready[0].SeriesText}";
: IsRtty
? $"{SeriesHeader} - {station.Text}"
: SeriesHeader;
for (int at = 0; at < rows.Count; at++)
{
bool has = at < ready.Count;
@@ -353,7 +365,7 @@ public sealed partial class QtcWindow : Window
{
if (isSending)
{
if (IsCw)
if (IsCw || IsRtty)
{
SendLine(at);
return;
@@ -375,11 +387,13 @@ public sealed partial class QtcWindow : Window
private void SendLine(int at)
{
lastSent = at;
_ = send(QtcMessages.Line(
rows[at].Time.Text ?? "",
rows[at].Call.Text ?? "",
rows[at].Number.Text ?? "",
session.Settings.QtcCwFieldSpacing));
_ = send(IsRtty
? QtcMessages.SendOne(session.Settings.QtcRttySpacing, RttyLine(at))
: QtcMessages.Line(
rows[at].Time.Text ?? "",
rows[at].Call.Text ?? "",
rows[at].Number.Text ?? "",
session.Settings.QtcCwFieldSpacing));
for (int next = at + 1; next < rows.Count; next++)
{
if (rows[next].Again.IsEffectivelyEnabled)
@@ -391,6 +405,43 @@ public sealed partial class QtcWindow : Window
CloseButton.Focus();
}
private string RttyLine(int at) => QtcMessages.RttyLine(
rows[at].Time.Text ?? "",
rows[at].Call.Text ?? "",
rows[at].Number.Text ?? "");
/// N1MM's Send All: the whole series in one message. RTTY runs at 45 baud,
/// so a series is the best part of a minute of transmission, and sending it
/// a line at a time means the operator presses a button between each one
/// while the transmitter is up. The heading, the spacing between the lines
/// and the ending are the operator's, and the message keys the transmitter
/// and drops it again itself.
private void OnSendAll(object? sender, RoutedEventArgs e)
{
List<string> lines = [];
for (int at = 0; at < rows.Count; at++)
{
if (!rows[at].IsEmpty)
{
lines.Add(RttyLine(at));
}
}
string message = QtcMessages.SendAll(
session.Settings.QtcRttySendAllHeading,
session.Settings.QtcRttySendAllEnding,
session.Settings.QtcRttySpacing,
SeriesHeader,
lines);
if (message.Length == 0)
{
StatusText.Text = "nothing to send";
return;
}
lastSent = lines.Count - 1;
_ = send(message);
CloseButton.Focus();
}
/// 1, 2, 3 and 4 as N1MM numbers them: the time, the call, the serial
/// number and the header.
private static int FieldOf(Key key) =>