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:
@@ -169,6 +169,16 @@ public sealed record Settings
|
||||
|
||||
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
|
||||
/// in `BandPlan.Default`; an entry replaces one band's boundaries.
|
||||
public IReadOnlyList<StoredSubBand> SubBands { get; init; } = [];
|
||||
|
||||
@@ -33,8 +33,19 @@
|
||||
</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."
|
||||
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"
|
||||
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">
|
||||
<Button Content="Cancel" Click="OnCancel" />
|
||||
<Button Content="Save" Click="OnSave" IsDefault="True" />
|
||||
|
||||
@@ -24,6 +24,9 @@ public sealed partial class QtcSetupDialog : Window
|
||||
CallAgainBox.Text = settings.QtcCwCallAgain;
|
||||
NumberAgainBox.Text = settings.QtcCwNumberAgain;
|
||||
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
|
||||
@@ -40,6 +43,9 @@ public sealed partial class QtcSetupDialog : Window
|
||||
QtcCwCallAgain = CallAgainBox.Text ?? "",
|
||||
QtcCwNumberAgain = NumberAgainBox.Text ?? "",
|
||||
QtcCwTu = TuBox.Text ?? "",
|
||||
QtcRttySpacing = RttySpacingBox.Text ?? "",
|
||||
QtcRttySendAllHeading = SendAllHeadingBox.Text ?? "",
|
||||
QtcRttySendAllEnding = SendAllEndingBox.Text ?? "",
|
||||
});
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<Button Name="ReadyButton" Content="RX Ready" Click="OnReady" />
|
||||
<Button Name="HeaderAgainButton" Content="Hdr Agn" Click="OnHeaderAgain" />
|
||||
<Button Name="HeaderCfmButton" Content="Cfm" Click="OnHeaderConfirm" />
|
||||
<Button Name="SendAllButton" Content="Send All" Click="OnSendAll" />
|
||||
</StackPanel>
|
||||
<Grid Name="Lines" Margin="0,6,0,0" />
|
||||
<TextBlock Name="KeysText" FontSize="11" Opacity="0.7" Margin="0,6,0,0" />
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user