Send WAE QTC traffic on CW
The QTC window took traffic down and read it out of the log, but nothing left the window: on CW the operator had to send the lines by hand. It now puts them on the air through the entry window's keyer, the way N1MM's QTC window sends through its own entry window, so the same macros are expanded and the same transmitter is pointed at first. Sending a series: Send Hdr keys the header, Send1 to Send10 key one line each — the time, the callsign and the serial number — and Close keys the TU message when one is set. Taking one down: Hdr Agn asks for the header again, and shift with Enter in a box asks for that one field again and clears it. The messages and their defaults are N1MM's: AGN TIME, AGN CALL, AGN SERIAL, the hard-coded HDR AGN with its two spaces, and a TU that sends nothing until the operator writes one. So is the field spacing, written N1MM's way with S for a space. All five are in Setup, where the note about the missing settings used to be. Nothing is sent on SSB or RTTY. N1MM plays four recordings on SSB and sends RTTY from its digital window, and there is neither here, so on those modes the buttons move the cursor as before. RX Ready sends nothing on any mode, because a recording is all N1MM ever sends from it. Serial numbers go out as digits: N1MM can send cut numbers and nothing here does, in any message. Driven under Xvfb against a fake cwdaemon, in a WAE CW contest with four contacts logged. Sending, it keyed QTC 1/2, 2257 DL1AAA 01, 2257 DL2BBB 01, then QTC 2/2 with its two lines and TU DE W1ABC on close. Receiving, HDR AGN, AGN TIME, AGN CALL and AGN SERIAL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
31
README.md
31
README.md
@@ -554,15 +554,30 @@ edited. On CW and SSB the direction is fixed by the rules; on RTTY traffic goes
|
|||||||
both ways and the window has a switch.
|
both ways and the window has a switch.
|
||||||
|
|
||||||
Right-click the window for **Setup**: how many QTCs a series carries, whether
|
Right-click the window for **Setup**: how many QTCs a series carries, whether
|
||||||
the RX Ready and Cfm steps are stopped at, and whether Hdr Agn and Agn clear
|
the RX Ready and Cfm steps are stopped at, whether Hdr Agn and Agn clear what
|
||||||
what they ask for again. The defaults are N1MM's — both steps skipped, the
|
they ask for again, and the CW messages below. The defaults are N1MM's — both
|
||||||
header cleared, the line left alone.
|
steps skipped, the header cleared, the line left alone.
|
||||||
|
|
||||||
Nothing in the window transmits. On CW the operator sends from the entry window,
|
On CW the window puts the traffic on the air itself, through the same keyer a
|
||||||
on SSB by voice, and RTTY has no digital window yet, so Agn, Cfm and RX Ready
|
function key sends through:
|
||||||
move the cursor rather than putting anything on the air. N1MM's QTC settings for
|
|
||||||
sending — the CW messages for Agn and TU, the field spacing, the SSB recordings
|
| | |
|
||||||
and the RTTY templates — are left out with them.
|
|---|---|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
The scoring was checked against six real WAE logs, and agrees with N1MM on
|
The scoring was checked against six real WAE logs, and agrees with N1MM on
|
||||||
points and on weighted multipliers to the contact.
|
points and on weighted multipliers to the contact.
|
||||||
|
|||||||
@@ -39,17 +39,16 @@ 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
|
rules about which sections are retired, and getting that wrong throws away good
|
||||||
data. The file is read without them.
|
data. The file is read without them.
|
||||||
|
|
||||||
**The QTC window does not transmit.** Ctrl+Z opens it and traffic goes into the
|
**The QTC window transmits on CW only.** The header, the lines, the TU and the
|
||||||
log, but N1MM sends the QTC text itself: the lines on CW and RTTY, and recorded
|
four again messages go out through the keyer, with N1MM's messages and N1MM's
|
||||||
voice messages for QRV, Agn, Cfm and TU on SSB. Here the Agn, Cfm and RX Ready
|
defaults. Nothing goes out on SSB or RTTY: N1MM plays four recordings on SSB —
|
||||||
buttons only move the cursor. What is missing under it is the message templates
|
QRV, Agn, Cfm and TU — and sends RTTY from its digital window, and this program
|
||||||
for QTC traffic, a voice keyer, and the digital window.
|
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 setup covers the window, not the sending.** Right-click ▸ Setup has
|
**Cut numbers are not sent.** N1MM can key a serial number as letters — `N` for
|
||||||
the four settings that change how the window behaves, with N1MM's defaults.
|
9, `T` for 0 — and offers several styles. Nothing here does, in a QTC line or in
|
||||||
What is not there is everything about putting QTCs on the air: N1MM's CW
|
any other message.
|
||||||
messages for Agn and TU, its SQTC field spacing, its four SSB recordings, and
|
|
||||||
its RTTY message templates. Those wait on a transmit path.
|
|
||||||
|
|
||||||
**Two WAE numbers do not agree with N1MM, both from 2020 and 2021 logs.** The
|
**Two WAE numbers do not agree with N1MM, both from 2020 and 2021 logs.** The
|
||||||
disagreements are the country file of the day, not the rules: 4U1A counted as
|
disagreements are the country file of the day, not the rules: 4U1A counted as
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Nonemm.Core;
|
using Nonemm.Core;
|
||||||
|
using Nonemm.Session;
|
||||||
|
|
||||||
namespace Nonemm.App.Configuration;
|
namespace Nonemm.App.Configuration;
|
||||||
|
|
||||||
@@ -155,6 +156,19 @@ public sealed record Settings
|
|||||||
|
|
||||||
public bool QtcAgainClearsLine { get; init; }
|
public bool QtcAgainClearsLine { get; init; }
|
||||||
|
|
||||||
|
/// The CW messages the QTC window sends, with N1MM's defaults. The spacing
|
||||||
|
/// is written N1MM's way, with `S` for a space. An empty TU message sends
|
||||||
|
/// nothing, which is N1MM's "Not Set".
|
||||||
|
public string QtcCwFieldSpacing { get; init; } = QtcMessages.DefaultFieldSpacing;
|
||||||
|
|
||||||
|
public string QtcCwTimeAgain { get; init; } = QtcMessages.DefaultTimeAgain;
|
||||||
|
|
||||||
|
public string QtcCwCallAgain { get; init; } = QtcMessages.DefaultCallAgain;
|
||||||
|
|
||||||
|
public string QtcCwNumberAgain { get; init; } = QtcMessages.DefaultNumberAgain;
|
||||||
|
|
||||||
|
public string QtcCwTu { get; init; } = "";
|
||||||
|
|
||||||
/// 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; } = [];
|
||||||
|
|||||||
@@ -18,8 +18,23 @@
|
|||||||
FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="24,-6,0,0" />
|
FontSize="11" Opacity="0.7" TextWrapping="Wrap" Margin="24,-6,0,0" />
|
||||||
<CheckBox Name="HeaderAgainClearsBox" Content="Hdr Agn clears the header" />
|
<CheckBox Name="HeaderAgainClearsBox" Content="Hdr Agn clears the header" />
|
||||||
<CheckBox Name="AgainClearsBox" Content="Agn clears the line" />
|
<CheckBox Name="AgainClearsBox" Content="Agn clears the line" />
|
||||||
|
<TextBlock Text="CW messages" FontWeight="Bold" Margin="0,10,0,0" />
|
||||||
|
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto" >
|
||||||
|
<TextBlock Text="Field spacing" VerticalAlignment="Center" Margin="0,2,8,2" />
|
||||||
|
<TextBox Grid.Column="1" Name="SpacingBox" Margin="0,2" />
|
||||||
|
<TextBlock Grid.Row="1" Text="Time again" VerticalAlignment="Center" Margin="0,2,8,2" />
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Name="TimeAgainBox" Margin="0,2" />
|
||||||
|
<TextBlock Grid.Row="2" Text="Call again" VerticalAlignment="Center" Margin="0,2,8,2" />
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Name="CallAgainBox" Margin="0,2" />
|
||||||
|
<TextBlock Grid.Row="3" Text="Serial again" VerticalAlignment="Center" Margin="0,2,8,2" />
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Name="NumberAgainBox" Margin="0,2" />
|
||||||
|
<TextBlock Grid.Row="4" Text="TU" VerticalAlignment="Center" Margin="0,2,8,2" />
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Name="TuBox" Margin="0,2" />
|
||||||
|
</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 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 other QTC settings are for sending: the CW messages for Agn and TU, the field spacing, the SSB recordings and the RTTY message templates. Nothing here transmits, so they are left out rather than shown as settings that do nothing." />
|
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." />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public sealed partial class QtcSetupDialog : Window
|
|||||||
SkipConfirmBox.IsChecked = settings.QtcSkipConfirm;
|
SkipConfirmBox.IsChecked = settings.QtcSkipConfirm;
|
||||||
HeaderAgainClearsBox.IsChecked = settings.QtcHeaderAgainClears;
|
HeaderAgainClearsBox.IsChecked = settings.QtcHeaderAgainClears;
|
||||||
AgainClearsBox.IsChecked = settings.QtcAgainClearsLine;
|
AgainClearsBox.IsChecked = settings.QtcAgainClearsLine;
|
||||||
|
SpacingBox.Text = settings.QtcCwFieldSpacing;
|
||||||
|
TimeAgainBox.Text = settings.QtcCwTimeAgain;
|
||||||
|
CallAgainBox.Text = settings.QtcCwCallAgain;
|
||||||
|
NumberAgainBox.Text = settings.QtcCwNumberAgain;
|
||||||
|
TuBox.Text = settings.QtcCwTu;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSave(object? sender, RoutedEventArgs e) => Close(settings with
|
private void OnSave(object? sender, RoutedEventArgs e) => Close(settings with
|
||||||
@@ -30,6 +35,11 @@ public sealed partial class QtcSetupDialog : Window
|
|||||||
QtcSkipConfirm = SkipConfirmBox.IsChecked == true,
|
QtcSkipConfirm = SkipConfirmBox.IsChecked == true,
|
||||||
QtcHeaderAgainClears = HeaderAgainClearsBox.IsChecked == true,
|
QtcHeaderAgainClears = HeaderAgainClearsBox.IsChecked == true,
|
||||||
QtcAgainClearsLine = AgainClearsBox.IsChecked == true,
|
QtcAgainClearsLine = AgainClearsBox.IsChecked == true,
|
||||||
|
QtcCwFieldSpacing = SpacingBox.Text ?? "",
|
||||||
|
QtcCwTimeAgain = TimeAgainBox.Text ?? "",
|
||||||
|
QtcCwCallAgain = CallAgainBox.Text ?? "",
|
||||||
|
QtcCwNumberAgain = NumberAgainBox.Text ?? "",
|
||||||
|
QtcCwTu = TuBox.Text ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||||
|
|||||||
@@ -86,6 +86,35 @@ public sealed partial class EntryWindow
|
|||||||
await SendThenAsync(session.Keyer, text, plan.After);
|
await SendThenAsync(session.Keyer, text, plan.After);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends a message that did not come from a function key. The QTC window
|
||||||
|
/// hands its traffic over this way, as N1MM's does through its own entry
|
||||||
|
/// window, so it goes out through the same keyer with the same macros
|
||||||
|
/// expanded.
|
||||||
|
private async Task SendTextAsync(string template)
|
||||||
|
{
|
||||||
|
if (Logging is null || template.Trim().Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MessagePlan plan = MessagePlan.Read(template, Logging, session.Other(Logging));
|
||||||
|
foreach (MessageAction action in plan.Before)
|
||||||
|
{
|
||||||
|
Run(action);
|
||||||
|
}
|
||||||
|
if (plan.Text.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (session.Keyer is null)
|
||||||
|
{
|
||||||
|
Status("no keyer — Config ▸ Keyer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await session.PointTransmitAtAsync(radioNumber);
|
||||||
|
Status($"sending {plan.Text}");
|
||||||
|
await SendThenAsync(session.Keyer, plan.Text, plan.After);
|
||||||
|
}
|
||||||
|
|
||||||
/// Hands the text to the keyer, and leaves what follows `{END}` to run on
|
/// Hands the text to the keyer, and leaves what follows `{END}` to run on
|
||||||
/// its own once the keyer says the message has gone out.
|
/// its own once the keyer says the message has gone out.
|
||||||
private async Task SendThenAsync(MessageSender keyer, string text, IReadOnlyList<MessageAction> after)
|
private async Task SendThenAsync(MessageSender keyer, string text, IReadOnlyList<MessageAction> after)
|
||||||
|
|||||||
@@ -1015,7 +1015,7 @@ public sealed partial class EntryWindow : Window
|
|||||||
Status("nothing left to report to " + station.Text);
|
Status("nothing left to report to " + station.Text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QtcWindow window = new(session, Logging, station, direction);
|
QtcWindow window = new(session, Logging, station, direction, SendTextAsync);
|
||||||
bool saved = await window.ShowDialog<bool>(this);
|
bool saved = await window.ShowDialog<bool>(this);
|
||||||
Logging.Wipe();
|
Logging.Wipe();
|
||||||
SyncBoxes();
|
SyncBoxes();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<TextBlock Text="QTC Header :" FontSize="11" Opacity="0.7" />
|
<TextBlock Text="QTC Header :" FontSize="11" Opacity="0.7" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<TextBox Name="HeaderBox" Width="170" />
|
<TextBox Name="HeaderBox" Width="170" />
|
||||||
|
<Button Name="HeaderSendButton" Content="Send Hdr" Click="OnHeaderSend" />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ namespace Nonemm.App.Windows;
|
|||||||
///
|
///
|
||||||
/// Receiving, the lines are typed as they arrive. Sending, they are filled from
|
/// Receiving, the lines are typed as they arrive. Sending, they are filled from
|
||||||
/// 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.
|
||||||
/// Nothing here transmits: on CW the operator sends from the entry window, on
|
///
|
||||||
/// SSB by voice, on RTTY from the digital window there is not yet.
|
/// 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.
|
||||||
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);
|
||||||
@@ -31,19 +33,24 @@ public sealed partial class QtcWindow : Window
|
|||||||
private readonly OperatingPosition position;
|
private readonly OperatingPosition position;
|
||||||
private readonly QtcTraffic traffic;
|
private readonly QtcTraffic traffic;
|
||||||
private readonly Callsign station;
|
private readonly Callsign station;
|
||||||
|
private readonly Func<string, Task> send;
|
||||||
private bool isSending;
|
private bool isSending;
|
||||||
private readonly List<Row> rows = [];
|
private readonly List<Row> rows = [];
|
||||||
private readonly List<WaeQtc> ready = [];
|
private readonly List<WaeQtc> ready = [];
|
||||||
|
|
||||||
|
/// `send` puts a message on the air through the entry window this was
|
||||||
|
/// opened from, so QTC traffic goes out the same way a function key does.
|
||||||
public QtcWindow(
|
public QtcWindow(
|
||||||
AppSession session,
|
AppSession session,
|
||||||
OperatingPosition position,
|
OperatingPosition position,
|
||||||
Callsign station,
|
Callsign station,
|
||||||
QtcDirection direction)
|
QtcDirection direction,
|
||||||
|
Func<string, Task> send)
|
||||||
{
|
{
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.station = station;
|
this.station = station;
|
||||||
|
this.send = send;
|
||||||
isSending = direction == QtcDirection.Send;
|
isSending = direction == QtcDirection.Send;
|
||||||
traffic = new QtcTraffic(position);
|
traffic = new QtcTraffic(position);
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -65,6 +72,10 @@ public sealed partial class QtcWindow : Window
|
|||||||
: session.Settings.QtcSkipReady ? HeaderBox
|
: session.Settings.QtcSkipReady ? HeaderBox
|
||||||
: ReadyButton;
|
: ReadyButton;
|
||||||
|
|
||||||
|
/// 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];
|
||||||
|
|
||||||
private void SetDirection(bool sending)
|
private void SetDirection(bool sending)
|
||||||
{
|
{
|
||||||
if (sending == isSending)
|
if (sending == isSending)
|
||||||
@@ -86,6 +97,7 @@ public sealed partial class QtcWindow : Window
|
|||||||
ReadyButton.IsVisible = !isSending;
|
ReadyButton.IsVisible = !isSending;
|
||||||
HeaderAgainButton.IsVisible = !isSending;
|
HeaderAgainButton.IsVisible = !isSending;
|
||||||
HeaderCfmButton.IsVisible = !isSending;
|
HeaderCfmButton.IsVisible = !isSending;
|
||||||
|
HeaderSendButton.IsVisible = isSending && IsCw;
|
||||||
HeaderBox.IsReadOnly = isSending;
|
HeaderBox.IsReadOnly = isSending;
|
||||||
BuildRows();
|
BuildRows();
|
||||||
Fill();
|
Fill();
|
||||||
@@ -108,7 +120,11 @@ public sealed partial class QtcWindow : Window
|
|||||||
TextBox time = Box(at, 0);
|
TextBox time = Box(at, 0);
|
||||||
TextBox call = Box(at, 1);
|
TextBox call = Box(at, 1);
|
||||||
TextBox number = Box(at, 2);
|
TextBox number = Box(at, 2);
|
||||||
Button again = new() { Content = $"Agn{at + 1}", Margin = new Avalonia.Thickness(2, 1, 2, 1) };
|
Button again = new()
|
||||||
|
{
|
||||||
|
Content = isSending && IsCw ? $"Send{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) };
|
Button confirm = new() { Content = $"Cfm{at + 1}", Margin = new Avalonia.Thickness(2, 1, 2, 1) };
|
||||||
int line = at;
|
int line = at;
|
||||||
again.Click += (_, _) => OnLineAgain(line);
|
again.Click += (_, _) => OnLineAgain(line);
|
||||||
@@ -222,6 +238,14 @@ public sealed partial class QtcWindow : Window
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
Close(false);
|
Close(false);
|
||||||
break;
|
break;
|
||||||
|
case Key.Enter when e.Source is TextBox box
|
||||||
|
&& !isSending
|
||||||
|
&& IsCw
|
||||||
|
&& e.KeyModifiers.HasFlag(KeyModifiers.Shift)
|
||||||
|
&& box != HeaderBox:
|
||||||
|
e.Handled = true;
|
||||||
|
SendFieldAgain(box);
|
||||||
|
break;
|
||||||
case Key.Enter when e.Source is TextBox:
|
case Key.Enter when e.Source is TextBox:
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
MoveOn();
|
MoveOn();
|
||||||
@@ -262,12 +286,24 @@ public sealed partial class QtcWindow : Window
|
|||||||
CloseButton.Focus();
|
CloseButton.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// N1MM's RX Ready tells the other station to go ahead; with no transmit
|
/// N1MM's RX Ready tells the other station to go ahead, and does it with a
|
||||||
/// path here it only puts the cursor where the first line will land.
|
/// 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();
|
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)
|
||||||
|
{
|
||||||
|
_ = send(HeaderBox.Text ?? "");
|
||||||
|
rows[0].Again.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnHeaderAgain(object? sender, RoutedEventArgs e)
|
private void OnHeaderAgain(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (IsCw)
|
||||||
|
{
|
||||||
|
_ = send(QtcMessages.HeaderAgain);
|
||||||
|
}
|
||||||
if (session.Settings.QtcHeaderAgainClears)
|
if (session.Settings.QtcHeaderAgainClears)
|
||||||
{
|
{
|
||||||
HeaderBox.Text = "";
|
HeaderBox.Text = "";
|
||||||
@@ -283,6 +319,11 @@ public sealed partial class QtcWindow : Window
|
|||||||
{
|
{
|
||||||
if (isSending)
|
if (isSending)
|
||||||
{
|
{
|
||||||
|
if (IsCw)
|
||||||
|
{
|
||||||
|
SendLine(at);
|
||||||
|
return;
|
||||||
|
}
|
||||||
rows[at].Time.Focus();
|
rows[at].Time.Focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -294,6 +335,45 @@ public sealed partial class QtcWindow : Window
|
|||||||
Paint();
|
Paint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads one line out and moves to the next: the time, the callsign and the
|
||||||
|
/// serial number, with the operator's spacing between them. When the last
|
||||||
|
/// line has gone the cursor lands on Close, which sends the TU message.
|
||||||
|
private void SendLine(int at)
|
||||||
|
{
|
||||||
|
_ = send(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)
|
||||||
|
{
|
||||||
|
rows[next].Again.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
int column = rows.Select(r => r.Time).Contains(box) ? 0
|
||||||
|
: rows.Select(r => r.Call).Contains(box) ? 1
|
||||||
|
: 2;
|
||||||
|
_ = send(column switch
|
||||||
|
{
|
||||||
|
0 => session.Settings.QtcCwTimeAgain,
|
||||||
|
1 => session.Settings.QtcCwCallAgain,
|
||||||
|
_ => session.Settings.QtcCwNumberAgain,
|
||||||
|
});
|
||||||
|
box.Text = "";
|
||||||
|
box.Focus();
|
||||||
|
Paint();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLineConfirm(int at)
|
private void OnLineConfirm(int at)
|
||||||
{
|
{
|
||||||
Control next = at + 1 < rows.Count ? rows[at + 1].Time : CloseButton;
|
Control next = at + 1 < rows.Count ? rows[at + 1].Time : CloseButton;
|
||||||
@@ -328,6 +408,10 @@ public sealed partial class QtcWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Save(lines);
|
Save(lines);
|
||||||
|
if (isSending && IsCw && session.Settings.QtcCwTu.Trim().Length > 0)
|
||||||
|
{
|
||||||
|
_ = send(session.Settings.QtcCwTu);
|
||||||
|
}
|
||||||
Close(true);
|
Close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
src/Nonemm.Session/QtcMessages.cs
Normal file
36
src/Nonemm.Session/QtcMessages.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace Nonemm.Session;
|
||||||
|
|
||||||
|
/// What a station sends on CW while exchanging QTC traffic, and N1MM's
|
||||||
|
/// defaults for the parts an operator can change.
|
||||||
|
///
|
||||||
|
/// CW only. N1MM sends recorded voice messages on SSB and its digital window's
|
||||||
|
/// templates on RTTY, and this program has neither.
|
||||||
|
public static class QtcMessages
|
||||||
|
{
|
||||||
|
/// The gap between the fields of a QTC line, written the way N1MM writes
|
||||||
|
/// it: `S` stands for a space, so `SS` is two of them and anything else
|
||||||
|
/// goes out as it is.
|
||||||
|
public const string DefaultFieldSpacing = "S";
|
||||||
|
|
||||||
|
public const string DefaultTimeAgain = "AGN TIME";
|
||||||
|
|
||||||
|
public const string DefaultCallAgain = "AGN CALL";
|
||||||
|
|
||||||
|
public const string DefaultNumberAgain = "AGN SERIAL";
|
||||||
|
|
||||||
|
/// Asking for the header again. N1MM writes this one into the program
|
||||||
|
/// rather than into its settings, two spaces and all.
|
||||||
|
public const string HeaderAgain = "HDR AGN";
|
||||||
|
|
||||||
|
public static string Spacing(string setting) =>
|
||||||
|
setting.Replace("S", " ", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// One line of traffic: the time, the callsign and the serial number. The
|
||||||
|
/// fields go out as they stand in the window, because what the window shows
|
||||||
|
/// is what the other station has to write down.
|
||||||
|
public static string Line(string time, string call, string number, string spacing)
|
||||||
|
{
|
||||||
|
string gap = Spacing(spacing);
|
||||||
|
return $"{time.Trim()}{gap}{call.Trim()}{gap}{number.Trim()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
34
tests/Nonemm.Session.Tests/QtcMessagesTests.cs
Normal file
34
tests/Nonemm.Session.Tests/QtcMessagesTests.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace Nonemm.Session.Tests;
|
||||||
|
|
||||||
|
public class QtcMessagesTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ALineIsTheTimeTheCallAndTheSerialNumber()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
"1234 DL1ABC 123",
|
||||||
|
QtcMessages.Line("1234", "DL1ABC", "123", QtcMessages.DefaultFieldSpacing));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N1MM writes the spacing with `S` for a space, so an operator who wants
|
||||||
|
/// two writes `SS`.
|
||||||
|
[Fact]
|
||||||
|
public void TheSpacingIsWrittenWithSForASpace()
|
||||||
|
{
|
||||||
|
Assert.Equal("1234 DL1ABC 123", QtcMessages.Line("1234", "DL1ABC", "123", "SS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnythingElseInTheSpacingGoesOutAsItIs()
|
||||||
|
{
|
||||||
|
Assert.Equal("1234/DL1ABC/123", QtcMessages.Line("1234", "DL1ABC", "123", "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WhatIsInTheBoxesIsTrimmed()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
"1234 DL1ABC 123",
|
||||||
|
QtcMessages.Line(" 1234 ", " DL1ABC ", " 123 ", QtcMessages.DefaultFieldSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user