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

@@ -355,13 +355,29 @@ of an SO2R station cannot call CQ by voice. Alternating CQ therefore works on CW
only, though nothing in it is CW-specific: a voice keyer that reports when the only, though nothing in it is CW-specific: a voice keyer that reports when the
recording has finished would drive it as it stands. recording has finished would drive it as it stands.
**The QTC window transmits on CW only.** The header, the lines, the QRV, the TU **The QTC window sends on CW and RTTY, not on SSB.** On CW the header, the
and the again messages go out through the keyer, with N1MM's messages and lines, the QRV, the TU and the again messages go out through the keyer, with
N1MM's defaults. Nothing goes out on SSB or RTTY: N1MM plays four recordings on N1MM's messages and N1MM's defaults.
SSB — QRV, Agn, Cfm and TU — and sends RTTY from its digital window, and this
program has neither a voice keyer nor a digital window. N1MM's Send All, which On RTTY the station reading traffic out has Send All, which is N1MM's: the whole
keys a whole series at once, belongs to that RTTY window and is not here series as one message, the heading, then every line with the operator's spacing
either. between them, then the ending, and `{TX}` and `{RX}` around the lot so it keys
the transmitter and drops it again. A line is the three fields joined with
hyphens, which is how N1MM writes them and it offers no setting for it. Snd n
sends one line again for a station that missed it. The heading, the ending and
the spacing are settings, N1MM's `WAESendAllHeadingText`,
`WAESendAllEndingText` and `WAESQTCSpacing`, with its defaults; `{ENTERLF}` in
them is new to the expander and stands for a carriage return and a line feed.
A whole series is the best part of a minute of transmission at 45 baud, which is
why it is one message rather than a button per line: the type-ahead buffer holds
it and paces the engine, and the operator presses one button. This has been
tested as text — what the message comes out as — and not on the air.
What is not here: N1MM's four SSB recordings — QRV, Agn, Cfm and TU — which need
a voice keyer, and its RTTY messages for the station taking traffic down, which
are `WAERXReadyText`, `WAEAllAgnText`, `WAEAGNText` and `WAESaveQTCText`. Those
four are typed into the digital window's transmit pane here.
**Cut numbers are not sent.** N1MM can key a serial number as letters — `N` for **Cut numbers are not sent.** N1MM can key a serial number as letters — `N` for
9, `T` for 0 — and offers several styles. Nothing here does, in a QTC line or in 9, `T` for 0 — and offers several styles. Nothing here does, in a QTC line or in
@@ -411,9 +427,11 @@ ones the station's log needs — and both of N1MM's names open a log with it. A
sprint log would be scored by the wrong rules, and a log started here writes sprint log would be scored by the wrong rules, and a log started here writes
`BARTGRTTYS` into the contest name, which N1MM would read as the sprint. `BARTGRTTYS` into the contest name, which N1MM would read as the sprint.
**Digital modes.** A contact can be logged as RTTY or another digital mode, and **Digital interfaces other than MMTTY.** A contact can be logged as RTTY or
the contest rules score it, but there is no digital window: no decoding, no another digital mode and the contest rules score it, and the digital window
transmitting, no interface to fldigi, MMTTY or similar. decodes and transmits through MMTTY over the Wine bridge. What is not here:
fldigi, MMVARI, 2Tone and the hardware TNCs, all of which N1MM drives.
`DigitalEngine` is the place to add them.
**Contest coverage.** Twenty-five families are built in, plus whatever `.udc` files **Contest coverage.** Twenty-five families are built in, plus whatever `.udc` files
are in the user-defined folder. N1MM ships well over a hundred. Opening a log are in the user-defined folder. N1MM ships well over a hundred. Opening a log

View File

@@ -169,6 +169,16 @@ public sealed record Settings
public string QtcCwTu { get; init; } = ""; public string QtcCwTu { get; init; } = "";
/// The RTTY messages the QTC window sends, with N1MM's defaults. The
/// spacing goes between the lines of a series rather than between the
/// fields of one, and all three are macro templates: `{ENTER}` for a
/// carriage return, `{QTC}` for the header of the series.
public string QtcRttySpacing { get; init; } = QtcMessages.DefaultRttySpacing;
public string QtcRttySendAllHeading { get; init; } = QtcMessages.DefaultSendAllHeading;
public string QtcRttySendAllEnding { get; init; } = QtcMessages.DefaultSendAllEnding;
/// Sub-band boundaries the operator has changed. Empty means the defaults /// Sub-band boundaries the operator has changed. Empty means the defaults
/// in `BandPlan.Default`; an entry replaces one band's boundaries. /// in `BandPlan.Default`; an entry replaces one band's boundaries.
public IReadOnlyList<StoredSubBand> SubBands { get; init; } = []; public IReadOnlyList<StoredSubBand> SubBands { get; init; } = [];

View File

@@ -33,8 +33,19 @@
</Grid> </Grid>
<TextBlock Text="The spacing goes between the fields of a QTC line, written N1MM's way with S for a space. The three again messages go out on shift and Enter in that box while taking traffic down. TU goes out when the window closes after reading a series out; empty sends nothing." <TextBlock Text="The spacing goes between the fields of a QTC line, written N1MM's way with S for a space. The three again messages go out on shift and Enter in that box while taking traffic down. TU goes out when the window closes after reading a series out; empty sends nothing."
FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="0,-2,0,0" /> FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="0,-2,0,0" />
<TextBlock Text="RTTY messages" FontWeight="Bold" Margin="0,10,0,0" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto">
<TextBlock Text="Line spacing" VerticalAlignment="Center" Margin="0,2,8,2" />
<TextBox Grid.Column="1" Name="RttySpacingBox" Margin="0,2" />
<TextBlock Grid.Row="1" Text="Send All heading" VerticalAlignment="Center" Margin="0,2,8,2" />
<TextBox Grid.Row="1" Grid.Column="1" Name="SendAllHeadingBox" Margin="0,2" />
<TextBlock Grid.Row="2" Text="Send All ending" VerticalAlignment="Center" Margin="0,2,8,2" />
<TextBox Grid.Row="2" Grid.Column="1" Name="SendAllEndingBox" Margin="0,2" />
</Grid>
<TextBlock Text="These go out when the All button reads a whole series out on RTTY. The spacing goes between the lines, not between the fields of one — use {ENTER} or {ENTERLF} for a carriage return. {QTC} stands for the header of the series, and the other function key macros work as well."
FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="0,-2,0,0" />
<TextBlock Name="MissingText" FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="0,6,0,0" <TextBlock Name="MissingText" FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="0,6,0,0"
Text="N1MM's SSB recordings and RTTY message templates are left out: sending those needs a voice keyer and a digital window, and this program has neither." /> Text="N1MM's SSB recordings are left out: playing those needs a voice keyer, and this program has none. So are its RTTY messages for the station taking traffic down — RX Ready, All Agn, Agn and Save — which are typed into the digital window's transmit pane here." />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,8,0,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,8,0,0">
<Button Content="Cancel" Click="OnCancel" /> <Button Content="Cancel" Click="OnCancel" />
<Button Content="Save" Click="OnSave" IsDefault="True" /> <Button Content="Save" Click="OnSave" IsDefault="True" />

View File

@@ -24,6 +24,9 @@ public sealed partial class QtcSetupDialog : Window
CallAgainBox.Text = settings.QtcCwCallAgain; CallAgainBox.Text = settings.QtcCwCallAgain;
NumberAgainBox.Text = settings.QtcCwNumberAgain; NumberAgainBox.Text = settings.QtcCwNumberAgain;
TuBox.Text = settings.QtcCwTu; TuBox.Text = settings.QtcCwTu;
RttySpacingBox.Text = settings.QtcRttySpacing;
SendAllHeadingBox.Text = settings.QtcRttySendAllHeading;
SendAllEndingBox.Text = settings.QtcRttySendAllEnding;
} }
private void OnSave(object? sender, RoutedEventArgs e) => Close(settings with private void OnSave(object? sender, RoutedEventArgs e) => Close(settings with
@@ -40,6 +43,9 @@ public sealed partial class QtcSetupDialog : Window
QtcCwCallAgain = CallAgainBox.Text ?? "", QtcCwCallAgain = CallAgainBox.Text ?? "",
QtcCwNumberAgain = NumberAgainBox.Text ?? "", QtcCwNumberAgain = NumberAgainBox.Text ?? "",
QtcCwTu = TuBox.Text ?? "", QtcCwTu = TuBox.Text ?? "",
QtcRttySpacing = RttySpacingBox.Text ?? "",
QtcRttySendAllHeading = SendAllHeadingBox.Text ?? "",
QtcRttySendAllEnding = SendAllEndingBox.Text ?? "",
}); });
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null); private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);

View File

@@ -21,6 +21,7 @@
<Button Name="ReadyButton" Content="RX Ready" Click="OnReady" /> <Button Name="ReadyButton" Content="RX Ready" Click="OnReady" />
<Button Name="HeaderAgainButton" Content="Hdr Agn" Click="OnHeaderAgain" /> <Button Name="HeaderAgainButton" Content="Hdr Agn" Click="OnHeaderAgain" />
<Button Name="HeaderCfmButton" Content="Cfm" Click="OnHeaderConfirm" /> <Button Name="HeaderCfmButton" Content="Cfm" Click="OnHeaderConfirm" />
<Button Name="SendAllButton" Content="Send All" Click="OnSendAll" />
</StackPanel> </StackPanel>
<Grid Name="Lines" Margin="0,6,0,0" /> <Grid Name="Lines" Margin="0,6,0,0" />
<TextBlock Name="KeysText" FontSize="11" Opacity="0.7" Margin="0,6,0,0" /> <TextBlock Name="KeysText" FontSize="11" Opacity="0.7" Margin="0,6,0,0" />

View File

@@ -19,8 +19,10 @@ namespace Nonemm.App.Windows;
/// the log and cannot be edited — what is being reported is what was worked. /// 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 /// 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 /// way N1MM's QTC window sends through its own entry window. On RTTY it sends
/// nothing is sent: one needs a voice keyer and the other a digital window. /// 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 public sealed partial class QtcWindow : Window
{ {
private static IBrush Saved => Themes.Brush(Themes.Current.GoodBackground); private static IBrush Saved => Themes.Brush(Themes.Current.GoodBackground);
@@ -83,7 +85,9 @@ public sealed partial class QtcWindow : Window
if (!IsCw) if (!IsCw)
{ {
return isSending 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."; : "Type the header, then a line per QTC: time, callsign, serial number.";
} }
return isSending return isSending
@@ -91,10 +95,17 @@ public sealed partial class QtcWindow : Window
: "Shift 1 = ask time Shift 2 = ask call Shift 3 = ask serial"; : "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 /// Which mode the traffic goes out on. The contest says, not what the radio
/// what the radio happens to be on, which is how N1MM decides too. /// happens to be on, which is how N1MM decides too.
private bool IsCw => position.Contest.Modes is [ModeCategory.Cw]; 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) private void SetDirection(bool sending)
{ {
if (sending == isSending) if (sending == isSending)
@@ -118,6 +129,9 @@ public sealed partial class QtcWindow : Window
HeaderAgainButton.Content = isSending && IsCw ? "Snd Hdr" : "Hdr Agn"; HeaderAgainButton.Content = isSending && IsCw ? "Snd Hdr" : "Hdr Agn";
HeaderAgainButton.IsVisible = !isSending || IsCw; HeaderAgainButton.IsVisible = !isSending || IsCw;
HeaderCfmButton.IsVisible = !isSending; 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; ClearButton.IsVisible = !isSending;
CloseButton.Content = IsCw ? "Exit" : "Close"; CloseButton.Content = IsCw ? "Exit" : "Close";
HeaderBox.IsReadOnly = isSending; HeaderBox.IsReadOnly = isSending;
@@ -144,7 +158,7 @@ public sealed partial class QtcWindow : Window
TextBox number = Box(at, 2); TextBox number = Box(at, 2);
Button again = new() 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), Margin = new Avalonia.Thickness(2, 1, 2, 1),
}; };
Button confirm = new() { Content = $"Cfm{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; return;
} }
ready.AddRange(traffic.ToSend(station, session.Settings.QtcLinesPerSeries)); 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 HeaderBox.Text = ready.Count == 0
? "" ? ""
: rtty : IsRtty
? $"QTC {ready[0].SeriesText} - {station.Text}" ? $"{SeriesHeader} - {station.Text}"
: $"QTC {ready[0].SeriesText}"; : SeriesHeader;
for (int at = 0; at < rows.Count; at++) for (int at = 0; at < rows.Count; at++)
{ {
bool has = at < ready.Count; bool has = at < ready.Count;
@@ -353,7 +365,7 @@ public sealed partial class QtcWindow : Window
{ {
if (isSending) if (isSending)
{ {
if (IsCw) if (IsCw || IsRtty)
{ {
SendLine(at); SendLine(at);
return; return;
@@ -375,7 +387,9 @@ public sealed partial class QtcWindow : Window
private void SendLine(int at) private void SendLine(int at)
{ {
lastSent = at; lastSent = at;
_ = send(QtcMessages.Line( _ = send(IsRtty
? QtcMessages.SendOne(session.Settings.QtcRttySpacing, RttyLine(at))
: QtcMessages.Line(
rows[at].Time.Text ?? "", rows[at].Time.Text ?? "",
rows[at].Call.Text ?? "", rows[at].Call.Text ?? "",
rows[at].Number.Text ?? "", rows[at].Number.Text ?? "",
@@ -391,6 +405,43 @@ public sealed partial class QtcWindow : Window
CloseButton.Focus(); 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 /// 1, 2, 3 and 4 as N1MM numbers them: the time, the call, the serial
/// number and the header. /// number and the header.
private static int FieldOf(Key key) => private static int FieldOf(Key key) =>

View File

@@ -113,6 +113,9 @@ public static class MessageExpander
// the digital window's carriage return, which starts a new line on // the digital window's carriage return, which starts a new line on
// the other station's screen // the other station's screen
"ENTER" => "\r", "ENTER" => "\r",
// the same with a line feed behind it, which is what N1MM's own
// RTTY message defaults are written with
"ENTERLF" => "\r\n",
_ => null, _ => null,
}; };

View File

@@ -1,10 +1,15 @@
using System.Text;
namespace Nonemm.Session; namespace Nonemm.Session;
/// What a station sends on CW while exchanging QTC traffic, and N1MM's /// What a station sends while exchanging QTC traffic, and N1MM's defaults for
/// defaults for the parts an operator can change. /// the parts an operator can change.
/// ///
/// CW only. N1MM sends recorded voice messages on SSB and its digital window's /// CW and RTTY. The two are laid out differently: on CW a line is the three
/// templates on RTTY, and this program has neither. /// fields with the operator's spacing between them and each one is keyed by
/// hand, and on RTTY the fields are joined with hyphens and the whole series
/// goes out as one message. N1MM plays recordings on SSB, and there is no voice
/// keyer here.
public static class QtcMessages public static class QtcMessages
{ {
/// The gap between the fields of a QTC line, written the way N1MM writes /// The gap between the fields of a QTC line, written the way N1MM writes
@@ -28,6 +33,59 @@ public static class QtcMessages
/// What the station reading traffic out asks before it starts. /// What the station reading traffic out asks before it starts.
public const string AreYouReady = "QRV?"; public const string AreYouReady = "QRV?";
/// What goes between the QTC lines on RTTY. N1MM keeps it as a macro rather
/// than as characters, so an operator can put a carriage return there,
/// which is what its default does.
public const string DefaultRttySpacing = "{ENTER}";
/// What stands in front of the series and what closes it. `{QTC}` stands
/// for the header, so the default reads the series out twice for a station
/// that missed it the first time.
public const string DefaultSendAllHeading = "{ENTERLF}{QTC} {QTC}";
public const string DefaultSendAllEnding = "{ENTERLF}QSL?? BK DE {MYCALL} K";
/// One line of traffic on RTTY: the three fields joined with hyphens. N1MM
/// writes them this way and offers no setting for it, so neither does this.
public static string RttyLine(string time, string call, string number) =>
$"{time.Trim()}-{call.Trim()}-{number.Trim()}";
/// A whole series as one message, which is N1MM's Send All: the heading,
/// then every line, then the ending, with `spacing` between them. It keys
/// the transmitter itself and drops it at the end, so the operator presses
/// one button and the series goes out.
///
/// The text macros in it are left as they stand — `{ENTER}`, `{MYCALL}` and
/// the rest are filled in by the message expander, the same as in a
/// function key. `{QTC}` is filled in here, because only this window knows
/// which series is going out.
///
/// A series with no lines in it sends nothing.
public static string SendAll(
string heading,
string ending,
string spacing,
string header,
IReadOnlyList<string> lines)
{
if (lines.Count == 0)
{
return "";
}
StringBuilder text = new();
text.Append("{TX}").Append(WithHeader(heading, header)).Append(spacing);
foreach (string line in lines)
{
text.Append(line).Append(' ').Append(spacing);
}
return text.Append(WithHeader(ending, header)).Append("{RX}").ToString();
}
/// One line of a series on its own, for a station that asks for it again.
/// It keys and drops the transmitter the same way Send All does.
public static string SendOne(string spacing, string line) =>
$"{{TX}}{spacing}{line}{spacing}{{RX}}";
public static string Spacing(string setting) => public static string Spacing(string setting) =>
setting.Replace("S", " ", StringComparison.Ordinal); setting.Replace("S", " ", StringComparison.Ordinal);
@@ -43,6 +101,11 @@ public static class QtcMessages
/// The message that ends the exchange, sent whichever way the traffic went. /// The message that ends the exchange, sent whichever way the traffic went.
/// `{QTC}` in it stands for the header of the series, so `TU {QTC} 73` /// `{QTC}` in it stands for the header of the series, so `TU {QTC} 73`
/// goes out as `TU QTC 3/10 73`. /// goes out as `TU QTC 3/10 73`.
public static string Tu(string template, string header) => public static string Tu(string template, string header) => WithHeader(template, header);
/// `{QTC}` filled in with the header of the series. It is filled in here
/// rather than by the message expander because only the QTC window knows
/// which series is going out.
private static string WithHeader(string template, string header) =>
template.Replace("{QTC}", header.Trim().ToUpperInvariant(), StringComparison.OrdinalIgnoreCase); template.Replace("{QTC}", header.Trim().ToUpperInvariant(), StringComparison.OrdinalIgnoreCase);
} }

View File

@@ -100,6 +100,14 @@ public class MessageExpanderTests
public void WithOneRadioTheOtherMacrosStandForNothing() => public void WithOneRadioTheOtherMacrosStandForNothing() =>
Assert.Equal("", MessageExpander.Expand("{OTHERFREQ}{OTHERMHZ}{OTHERBAND}", Session())); Assert.Equal("", MessageExpander.Expand("{OTHERFREQ}{OTHERMHZ}{OTHERBAND}", Session()));
/// The two carriage return macros a digital message is written with. N1MM's
/// own RTTY message defaults use `{ENTERLF}`.
[Fact]
public void TheCarriageReturnMacrosAreTheCharactersTheyStandFor()
{
Assert.Equal("A\rB\r\nC", MessageExpander.Expand("A{ENTER}B{ENTERLF}C", Session()));
}
[Fact] [Fact]
public void TheNameComesFromTheCallHistoryWhenTheContestHasNoNameBox() public void TheNameComesFromTheCallHistoryWhenTheContestHasNoNameBox()
{ {

View File

@@ -37,6 +37,57 @@ public class QtcMessagesTests
Assert.Equal("TU 73", QtcMessages.Tu("TU 73", "QTC 3/10")); Assert.Equal("TU 73", QtcMessages.Tu("TU 73", "QTC 3/10"));
} }
/// N1MM joins the three fields with hyphens on RTTY and offers no setting
/// for it.
[Fact]
public void ARttyLineIsTheThreeFieldsJoinedWithHyphens()
{
Assert.Equal("1234-DL1ABC-123", QtcMessages.RttyLine(" 1234 ", " DL1ABC ", " 123 "));
}
/// The shape is N1MM's, from `QTCWindow.cs:3609`: the heading, the spacing,
/// then every line with a space and the spacing behind it, then the ending.
/// The macros are left for the expander; `{QTC}` is the one filled in here.
[Fact]
public void SendAllReadsTheWholeSeriesOutInOneMessage()
{
string message = QtcMessages.SendAll(
QtcMessages.DefaultSendAllHeading,
QtcMessages.DefaultSendAllEnding,
QtcMessages.DefaultRttySpacing,
"QTC 3/10",
["1234-DL1ABC-123", "1240-G3XYZ-124"]);
Assert.Equal(
"{TX}{ENTERLF}QTC 3/10 QTC 3/10{ENTER}"
+ "1234-DL1ABC-123 {ENTER}"
+ "1240-G3XYZ-124 {ENTER}"
+ "{ENTERLF}QSL?? BK DE {MYCALL} K{RX}",
message);
}
[Fact]
public void SendAllWithNoLinesSendsNothing()
{
Assert.Equal(
"",
QtcMessages.SendAll(
QtcMessages.DefaultSendAllHeading,
QtcMessages.DefaultSendAllEnding,
QtcMessages.DefaultRttySpacing,
"QTC 3/10",
[]));
}
/// One line asked for again, which N1MM wraps in the spacing on both sides.
[Fact]
public void OneLineKeysAndDropsTheTransmitterOfItsOwn()
{
Assert.Equal(
"{TX}{ENTER}1234-DL1ABC-123{ENTER}{RX}",
QtcMessages.SendOne(QtcMessages.DefaultRttySpacing, "1234-DL1ABC-123"));
}
[Fact] [Fact]
public void WhatIsInTheBoxesIsTrimmed() public void WhatIsInTheBoxesIsTrimmed()
{ {