Take the rest of what N1MM's QTC window does on CW
Reading the source again after this was tried turned up several things it had
wrong or left out.
The TU message went out only when reading a series out. N1MM sends it whichever
way the traffic went, and lets it name the series: {QTC} in it stands for the
header, so TU {QTC} 73 goes out as TU QTC 3/10 73.
The RX Ready button was hidden while sending and sent nothing on CW. It is
N1MM's QRV button: reading out it says R U QRV and keys QRV?, taking down it
says QRV and keys QRV.
The fields were asked for again with shift and Enter, which was made up. N1MM
uses the number keys, and writes them under the lines: 1, 2, 3 and 4 send the
time, call, serial or header of the last line again while reading out, and
shift with 1, 2 or 3 asks for that field while taking down.
The buttons now carry N1MM's names for the direction the traffic is going —
R U QRV, Snd Hdr, Snd1 to Snd10, Exit reading out; QRV, Hdr Agn, Agn1 to Agn10,
Exit taking down — and the Cfm column and Clear are hidden while reading out,
as N1MM hides them. The separate Send Hdr button is gone: N1MM has one button
there that sends the header one way and asks for it the other.
The log window gained N1MM's QTC? column for a contest that carries QTC
traffic. It holds the series, so a QTC row can be told from a contact; before
this a QTC row read as an ordinary one with odd values in the report columns.
Driven under Xvfb against a fake cwdaemon. Reading out: QRV?, QTC 3/2,
2319 DL7GGG 03, then 2319 and QTC 3/2 from the number keys, and
TU QTC 3/2 73 on Exit. Taking down: QRV, AGN TIME, AGN CALL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
43
README.md
43
README.md
@@ -559,25 +559,44 @@ they ask for again, and the CW messages below. The defaults are N1MM's — both
|
||||
steps skipped, the header cleared, the line left alone.
|
||||
|
||||
On CW the window puts the traffic on the air itself, through the same keyer a
|
||||
function key sends through:
|
||||
function key sends through. The buttons carry N1MM's names, which say what they
|
||||
do in the direction the traffic is going.
|
||||
|
||||
Reading a series out:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Send Hdr | the header of the series, `QTC 3/10` |
|
||||
| Send1 to Send10 | one line: the time, the callsign and the serial number |
|
||||
| Close | the TU message, when one is set |
|
||||
| Hdr Agn | `HDR AGN`, asking for the header again |
|
||||
| Shift and Enter in a box | asks for that one field again — `AGN TIME`, `AGN CALL`, `AGN SERIAL` — and clears it |
|
||||
| R U QRV | `QRV?`, asking whether the other station is ready |
|
||||
| Snd Hdr | the header of the series, `QTC 3/10` |
|
||||
| Snd1 to Snd10 | one line: the time, the callsign and the serial number |
|
||||
| 1, 2, 3, 4 | sends the time, the call, the serial or the header of the last line again, for a station that missed a piece of it |
|
||||
| Exit | saves the traffic and sends the TU message |
|
||||
|
||||
The gap between the fields of a line is written N1MM's way, with `S` for a
|
||||
space, so `SS` is two and anything else goes out as it stands. All five messages
|
||||
are N1MM's defaults and are edited in Setup. The serial number goes out as
|
||||
digits: N1MM can send cut numbers, and nothing here does, for any message.
|
||||
Taking one down:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| QRV | `QRV`, telling the other station to go ahead |
|
||||
| Hdr Agn | `HDR AGN`, asking for the header again |
|
||||
| Shift 1, Shift 2, Shift 3 | asks for that one field again — `AGN TIME`, `AGN CALL`, `AGN SERIAL` — and clears it |
|
||||
| Exit | saves the traffic and sends the TU message |
|
||||
|
||||
The TU message goes out whichever way the traffic went, and `{QTC}` in it stands
|
||||
for the header, so `TU {QTC} 73` goes out as `TU QTC 3/10 73`. It is empty out
|
||||
of the box, and then nothing is sent. The gap between the fields of a line is
|
||||
written N1MM's way, with `S` for a space, so `SS` is two and anything else goes
|
||||
out as it stands. All of it is edited in Setup, with N1MM's defaults. The serial
|
||||
number goes out as digits: N1MM can send cut numbers, and nothing here does, for
|
||||
any message.
|
||||
|
||||
Nothing is sent on SSB or RTTY. N1MM plays four recordings on SSB — QRV, Agn,
|
||||
Cfm and TU — and sends RTTY through its digital window; this program has no
|
||||
voice keyer and no digital window, so on those two modes Agn, Cfm and RX Ready
|
||||
move the cursor as they did before.
|
||||
voice keyer and no digital window, so on those two modes the buttons move the
|
||||
cursor as they did before. N1MM's RTTY window also has a Send All button that
|
||||
keys the whole series at once; there is nothing to send it with here.
|
||||
|
||||
In a WAE contest the log window has N1MM's **QTC?** column, which holds the
|
||||
series a QTC row belongs to and is empty for an ordinary contact.
|
||||
|
||||
The scoring was checked against six real WAE logs, and agrees with N1MM on
|
||||
points and on weighted multipliers to the contact.
|
||||
|
||||
@@ -39,12 +39,13 @@ and passed over. Acting on them means holding N1MM's section lists and its
|
||||
rules about which sections are retired, and getting that wrong throws away good
|
||||
data. The file is read without them.
|
||||
|
||||
**The QTC window transmits on CW only.** The header, the lines, the TU and the
|
||||
four again messages go out through the keyer, with N1MM's messages and N1MM's
|
||||
defaults. Nothing goes out on SSB or RTTY: N1MM plays four recordings on 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. RX Ready therefore sends nothing
|
||||
on any mode here, because N1MM only ever sends a recording from it.
|
||||
**The QTC window transmits on CW only.** The header, the lines, the QRV, the TU
|
||||
and the again messages go out through the keyer, with N1MM's messages and
|
||||
N1MM's defaults. Nothing goes out on SSB or RTTY: N1MM plays four recordings on
|
||||
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
|
||||
keys a whole series at once, belongs to that RTTY window and is not here
|
||||
either.
|
||||
|
||||
**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
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<TextBlock Text="QTC Header :" FontSize="11" Opacity="0.7" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBox Name="HeaderBox" Width="170" />
|
||||
<Button Name="HeaderSendButton" Content="Send Hdr" Click="OnHeaderSend" />
|
||||
<Button Name="ReadyButton" Content="RX Ready" Click="OnReady" />
|
||||
<Button Name="HeaderAgainButton" Content="Hdr Agn" Click="OnHeaderAgain" />
|
||||
<Button Name="HeaderCfmButton" Content="Cfm" Click="OnHeaderConfirm" />
|
||||
@@ -29,7 +28,7 @@
|
||||
Text="Enter, Tab move forward — space moves within a QTC line. Green = saved, red = not filled, yellow = format error." />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,6,0,0">
|
||||
<TextBlock Name="StatusText" VerticalAlignment="Center" FontSize="11" Opacity="0.7" />
|
||||
<Button Content="Clear" Click="OnClear" />
|
||||
<Button Name="ClearButton" Content="Clear" Click="OnClear" />
|
||||
<Button Name="CloseButton" Content="Close" Click="OnClose" IsDefault="True" />
|
||||
<Button Content="Cancel" Click="OnCancel" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -35,6 +35,10 @@ public sealed partial class QtcWindow : Window
|
||||
private readonly Callsign station;
|
||||
private readonly Func<string, Task> send;
|
||||
private bool isSending;
|
||||
|
||||
/// The line last read out, which is the one the number keys repeat a field
|
||||
/// of.
|
||||
private int lastSent;
|
||||
private readonly List<Row> rows = [];
|
||||
private readonly List<WaeQtc> ready = [];
|
||||
|
||||
@@ -72,6 +76,21 @@ public sealed partial class QtcWindow : Window
|
||||
: session.Settings.QtcSkipReady ? HeaderBox
|
||||
: ReadyButton;
|
||||
|
||||
/// N1MM writes the number keys under the lines. They only do anything on
|
||||
/// CW, so the line says something else on the other modes.
|
||||
private string KeysLine()
|
||||
{
|
||||
if (!IsCw)
|
||||
{
|
||||
return isSending
|
||||
? $"{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
|
||||
? "1 = time 2 = call 3 = serial 4 = header — sends that field of the line again"
|
||||
: "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.
|
||||
private bool IsCw => position.Contest.Modes is [ModeCategory.Cw];
|
||||
@@ -91,13 +110,16 @@ public sealed partial class QtcWindow : Window
|
||||
private void Reset()
|
||||
{
|
||||
Title = isSending ? $"Send QTC · {station.Text}" : $"Receive QTC · {station.Text}";
|
||||
KeysText.Text = isSending
|
||||
? $"{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.";
|
||||
ReadyButton.IsVisible = !isSending;
|
||||
HeaderAgainButton.IsVisible = !isSending;
|
||||
KeysText.Text = KeysLine();
|
||||
// N1MM's labels: the buttons say what they do in the direction the
|
||||
// traffic is going, and the ones with nothing to do are hidden
|
||||
ReadyButton.IsVisible = IsCw || !isSending;
|
||||
ReadyButton.Content = !IsCw ? "RX Ready" : isSending ? "R U QRV" : "QRV";
|
||||
HeaderAgainButton.Content = isSending && IsCw ? "Snd Hdr" : "Hdr Agn";
|
||||
HeaderAgainButton.IsVisible = !isSending || IsCw;
|
||||
HeaderCfmButton.IsVisible = !isSending;
|
||||
HeaderSendButton.IsVisible = isSending && IsCw;
|
||||
ClearButton.IsVisible = !isSending;
|
||||
CloseButton.Content = IsCw ? "Exit" : "Close";
|
||||
HeaderBox.IsReadOnly = isSending;
|
||||
BuildRows();
|
||||
Fill();
|
||||
@@ -122,15 +144,16 @@ public sealed partial class QtcWindow : Window
|
||||
TextBox number = Box(at, 2);
|
||||
Button again = new()
|
||||
{
|
||||
Content = isSending && IsCw ? $"Send{at + 1}" : $"Agn{at + 1}",
|
||||
Content = isSending && IsCw ? $"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) };
|
||||
int line = at;
|
||||
again.Click += (_, _) => OnLineAgain(line);
|
||||
confirm.Click += (_, _) => OnLineConfirm(line);
|
||||
// nothing to confirm when a RTTY station is reading its own log out
|
||||
confirm.IsVisible = !(isSending && position.Contest.Modes.Contains(ModeCategory.Digital));
|
||||
// nothing to confirm while reading our own log out, which is how
|
||||
// N1MM hides the column
|
||||
confirm.IsVisible = !isSending;
|
||||
Grid.SetRow(again, at);
|
||||
Grid.SetColumn(again, 3);
|
||||
Grid.SetRow(confirm, at);
|
||||
@@ -238,13 +261,15 @@ public sealed partial class QtcWindow : Window
|
||||
e.Handled = true;
|
||||
Close(false);
|
||||
break;
|
||||
case Key.Enter when e.Source is TextBox box
|
||||
&& !isSending
|
||||
&& IsCw
|
||||
&& e.KeyModifiers.HasFlag(KeyModifiers.Shift)
|
||||
&& box != HeaderBox:
|
||||
case >= Key.D1 and <= Key.D4 when IsCw && isSending:
|
||||
case >= Key.NumPad1 and <= Key.NumPad4 when IsCw && isSending:
|
||||
e.Handled = true;
|
||||
SendFieldAgain(box);
|
||||
SendField(FieldOf(e.Key));
|
||||
break;
|
||||
case >= Key.D1 and <= Key.D3 when IsCw && e.KeyModifiers.HasFlag(KeyModifiers.Shift):
|
||||
case >= Key.NumPad1 and <= Key.NumPad3 when IsCw && e.KeyModifiers.HasFlag(KeyModifiers.Shift):
|
||||
e.Handled = true;
|
||||
AskFieldAgain(FieldOf(e.Key));
|
||||
break;
|
||||
case Key.Enter when e.Source is TextBox:
|
||||
e.Handled = true;
|
||||
@@ -286,20 +311,29 @@ public sealed partial class QtcWindow : Window
|
||||
CloseButton.Focus();
|
||||
}
|
||||
|
||||
/// N1MM's RX Ready tells the other station to go ahead, and does it with a
|
||||
/// recording. There is no voice keyer here, so it only puts the cursor
|
||||
/// where the first line will land.
|
||||
private void OnReady(object? sender, RoutedEventArgs e) => rows[0].Time.Focus();
|
||||
|
||||
/// The header of the series we are about to read out: `QTC 3/10`.
|
||||
private void OnHeaderSend(object? sender, RoutedEventArgs e)
|
||||
/// Reading traffic out, this asks the other station whether it is ready;
|
||||
/// taking it down, it says we are. N1MM keys `QRV?` and `QRV` and plays a
|
||||
/// recording on SSB, which there is no keyer for here.
|
||||
private void OnReady(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = send(HeaderBox.Text ?? "");
|
||||
rows[0].Again.Focus();
|
||||
if (IsCw)
|
||||
{
|
||||
_ = send(isSending ? QtcMessages.AreYouReady : QtcMessages.ReadyToReceive);
|
||||
}
|
||||
Control next = isSending ? rows[0].Again : rows[0].Time;
|
||||
next.Focus();
|
||||
}
|
||||
|
||||
/// One button, as N1MM has it: reading traffic out it keys the header of
|
||||
/// the series, taking it down it asks for the header again.
|
||||
private void OnHeaderAgain(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (isSending)
|
||||
{
|
||||
_ = send(HeaderBox.Text ?? "");
|
||||
rows[0].Again.Focus();
|
||||
return;
|
||||
}
|
||||
if (IsCw)
|
||||
{
|
||||
_ = send(QtcMessages.HeaderAgain);
|
||||
@@ -340,6 +374,7 @@ public sealed partial class QtcWindow : Window
|
||||
/// line has gone the cursor lands on Close, which sends the TU message.
|
||||
private void SendLine(int at)
|
||||
{
|
||||
lastSent = at;
|
||||
_ = send(QtcMessages.Line(
|
||||
rows[at].Time.Text ?? "",
|
||||
rows[at].Call.Text ?? "",
|
||||
@@ -356,24 +391,58 @@ public sealed partial class QtcWindow : Window
|
||||
CloseButton.Focus();
|
||||
}
|
||||
|
||||
/// N1MM's Shift+Enter: ask for one field of the line being taken down
|
||||
/// again, and clear it. One message per column, all three in the settings.
|
||||
private void SendFieldAgain(TextBox box)
|
||||
/// 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) =>
|
||||
key is >= Key.NumPad1 and <= Key.NumPad4 ? key - Key.NumPad1 : key - Key.D1;
|
||||
|
||||
/// Reading traffic out, the number keys send one field of the line last
|
||||
/// sent again, for a station that missed a piece of it.
|
||||
private void SendField(int field)
|
||||
{
|
||||
int column = rows.Select(r => r.Time).Contains(box) ? 0
|
||||
: rows.Select(r => r.Call).Contains(box) ? 1
|
||||
: 2;
|
||||
_ = send(column switch
|
||||
if (field == 3)
|
||||
{
|
||||
_ = send(HeaderBox.Text ?? "");
|
||||
return;
|
||||
}
|
||||
Row row = rows[Math.Max(0, lastSent)];
|
||||
_ = send((field switch
|
||||
{
|
||||
0 => row.Time.Text,
|
||||
1 => row.Call.Text,
|
||||
_ => row.Number.Text,
|
||||
} ?? "").Trim());
|
||||
}
|
||||
|
||||
/// Taking traffic down, shift and a number ask for that field again and
|
||||
/// clear it. One message per field, all three in the settings.
|
||||
private void AskFieldAgain(int field)
|
||||
{
|
||||
_ = send(field switch
|
||||
{
|
||||
0 => session.Settings.QtcCwTimeAgain,
|
||||
1 => session.Settings.QtcCwCallAgain,
|
||||
_ => session.Settings.QtcCwNumberAgain,
|
||||
});
|
||||
box.Text = "";
|
||||
box.Focus();
|
||||
Paint();
|
||||
if (FocusedRow() is { } row)
|
||||
{
|
||||
TextBox box = field switch
|
||||
{
|
||||
0 => row.Time,
|
||||
1 => row.Call,
|
||||
_ => row.Number,
|
||||
};
|
||||
box.Text = "";
|
||||
box.Focus();
|
||||
Paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// The line the cursor is in, or the first one when it is somewhere else.
|
||||
private Row? FocusedRow() =>
|
||||
rows.FirstOrDefault(r => r.Time.IsFocused || r.Call.IsFocused || r.Number.IsFocused)
|
||||
?? rows.FirstOrDefault();
|
||||
|
||||
private void OnLineConfirm(int at)
|
||||
{
|
||||
Control next = at + 1 < rows.Count ? rows[at + 1].Time : CloseButton;
|
||||
@@ -408,9 +477,9 @@ public sealed partial class QtcWindow : Window
|
||||
}
|
||||
}
|
||||
Save(lines);
|
||||
if (isSending && IsCw && session.Settings.QtcCwTu.Trim().Length > 0)
|
||||
if (IsCw && session.Settings.QtcCwTu.Trim().Length > 0)
|
||||
{
|
||||
_ = send(session.Settings.QtcCwTu);
|
||||
_ = send(QtcMessages.Tu(session.Settings.QtcCwTu, HeaderBox.Text ?? ""));
|
||||
}
|
||||
Close(true);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ public interface Contest
|
||||
/// contests the same question.
|
||||
bool UsesItuZones => false;
|
||||
|
||||
/// True for a contest whose log carries QTC traffic as rows of its own.
|
||||
/// The log window then shows the series they belong to.
|
||||
bool HasQtcTraffic => false;
|
||||
|
||||
string SentExchangeFor(StationInfo me);
|
||||
|
||||
int PointsFor(QsoContext qso);
|
||||
|
||||
@@ -61,6 +61,8 @@ public sealed class Wae : Contest
|
||||
|
||||
public IReadOnlyList<ModeCategory> Modes => [mode];
|
||||
|
||||
public bool HasQtcTraffic => true;
|
||||
|
||||
public string SentExchangeFor(StationInfo me) => "001";
|
||||
|
||||
/// A QTC line counts, and so does every contact on a contest band. The
|
||||
|
||||
@@ -243,6 +243,12 @@ public sealed class QsoEditor
|
||||
kinds[mapped] = field.Kind;
|
||||
columns.Add(new QsoColumn(field.Label, mapped, Math.Max(field.Width, field.Label.Length + 1)));
|
||||
}
|
||||
if (contest.HasQtcTraffic)
|
||||
{
|
||||
// N1MM's header for the misc column in WAE, where it holds the
|
||||
// series a QTC row belongs to
|
||||
columns.Add(new QsoColumn("QTC?", QsoField.MiscText, 6));
|
||||
}
|
||||
columns.Add(new QsoColumn("Op", QsoField.Operator, 8));
|
||||
return columns;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ public static class QtcMessages
|
||||
/// rather than into its settings, two spaces and all.
|
||||
public const string HeaderAgain = "HDR AGN";
|
||||
|
||||
/// What the station taking traffic down sends to say it is ready.
|
||||
public const string ReadyToReceive = "QRV";
|
||||
|
||||
/// What the station reading traffic out asks before it starts.
|
||||
public const string AreYouReady = "QRV?";
|
||||
|
||||
public static string Spacing(string setting) =>
|
||||
setting.Replace("S", " ", StringComparison.Ordinal);
|
||||
|
||||
@@ -33,4 +39,10 @@ public static class QtcMessages
|
||||
string gap = Spacing(spacing);
|
||||
return $"{time.Trim()}{gap}{call.Trim()}{gap}{number.Trim()}";
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// goes out as `TU QTC 3/10 73`.
|
||||
public static string Tu(string template, string header) =>
|
||||
template.Replace("{QTC}", header.Trim().ToUpperInvariant(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,19 @@ public class QtcMessagesTests
|
||||
Assert.Equal("1234/DL1ABC/123", QtcMessages.Line("1234", "DL1ABC", "123", "/"));
|
||||
}
|
||||
|
||||
/// N1MM lets the TU message name the series it is closing.
|
||||
[Fact]
|
||||
public void TheTuMessageCanCarryTheHeader()
|
||||
{
|
||||
Assert.Equal("TU QTC 3/10 73", QtcMessages.Tu("TU {QTC} 73", "QTC 3/10"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ATuMessageWithoutTheHeaderIsLeftAlone()
|
||||
{
|
||||
Assert.Equal("TU 73", QtcMessages.Tu("TU 73", "QTC 3/10"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhatIsInTheBoxesIsTrimmed()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user