Take the rest of what reading N1MM turned up
The call frame. Above the callsign box, N1MM writes which spotted station the radio is sitting on, coloured like the bandmap colours it, or CQ-Frequency when this is the frequency the last CQ went out on. Space with an empty callsign box takes the call out of the frame and into the box, but only while searching: TabPressed does that when the run box is unchecked, because a running station is answering callers rather than chasing the one under its VFO. How close counts as sitting on it is N1MM's tuning tolerance, 300 Hz, now a setting. Sh/dx while unassisted. TelnetClient.Output refuses any command holding SH/DX when the entry is not assisted, and the telnet window does the same now. Whether an entry is assisted follows N1MM's own rule: assisted, multi-operator or checklog may use spots, a single operator who did not say assisted may not. The node's flavour. Click_LogonButton reads DXSpider, AR-Cluster, CC Cluster or GoCluster out of the banner; ClusterClient reads the same words and the window shows what it found. It changes nothing yet — the commands it would change are ones this does not send — but the node says so and now we know. CQ WW RTTY's state box. NextTab only stops on SectionText when the station is US or Canadian, because nobody else sends a state, so a contest can now say a box is not for this station and the walk steps over it. The box is still there to type in. The telnet button editor shows as many buttons as are defined, with an empty row to write the next one in, rather than N1MM's twelve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,11 @@ public sealed record Settings
|
||||
/// it has changed since it went out.
|
||||
public bool EsmSendsCorrectedCall { get; init; } = true;
|
||||
|
||||
/// How close to a spot the radio has to be for the entry window to say the
|
||||
/// station is there. N1MM asks for the same number, one per mode; this is
|
||||
/// one number for all of them.
|
||||
public int TuningToleranceHertz { get; init; } = 300;
|
||||
|
||||
/// How far `{FREQUP}` and `{FREQDN}` move the radio. N1MM asks for the same
|
||||
/// number in its Configurer.
|
||||
public int FrequencyStepHertz { get; init; } = 100;
|
||||
|
||||
84
src/Nonemm.App/Windows/EntryWindow.CallFrame.cs
Normal file
84
src/Nonemm.App/Windows/EntryWindow.CallFrame.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Session;
|
||||
using Nonemm.Spotting;
|
||||
|
||||
namespace Nonemm.App.Windows;
|
||||
|
||||
/// The line above the callsign box. N1MM calls it the call frame: it says which
|
||||
/// spotted station the radio is sitting on, coloured the way the bandmap
|
||||
/// colours it, or that this is the frequency we are calling CQ on. Space with
|
||||
/// an empty callsign box takes the station out of the frame and into the box,
|
||||
/// which is how a station is worked without typing its call.
|
||||
public sealed partial class EntryWindow
|
||||
{
|
||||
/// Where we last called CQ, so the frame can say so when the radio comes
|
||||
/// back to it.
|
||||
private Frequency cqFrequency;
|
||||
|
||||
/// What the frame holds, when it holds a callsign. `CQ-Frequency` is not
|
||||
/// one, and neither is nothing.
|
||||
private string framedCall = "";
|
||||
|
||||
private Frequency TuningTolerance =>
|
||||
Frequency.FromHertz(Math.Max(0, session.Settings.TuningToleranceHertz));
|
||||
|
||||
private void ShowCallFrame()
|
||||
{
|
||||
framedCall = "";
|
||||
if (Logging is null)
|
||||
{
|
||||
CallFrameText.Text = "";
|
||||
return;
|
||||
}
|
||||
if (cqFrequency.Hertz > 0
|
||||
&& Math.Abs(Logging.Frequency.Hertz - cqFrequency.Hertz) <= TuningTolerance.Hertz)
|
||||
{
|
||||
CallFrameText.Text = "CQ-Frequency";
|
||||
CallFrameText.Foreground = Verdicts.Worth;
|
||||
return;
|
||||
}
|
||||
if (session.Bandmap.Near(Logging.Frequency, TuningTolerance) is not { } spot)
|
||||
{
|
||||
CallFrameText.Text = "";
|
||||
return;
|
||||
}
|
||||
framedCall = spot.Call.Text;
|
||||
CallFrameText.Text = spot.IsSplit
|
||||
? $"{spot.Call.Text} — listening on {spot.Qsx.Kilohertz:0.0}"
|
||||
: spot.Call.Text;
|
||||
CallFrameText.Foreground = Verdicts.Colour(VerdictFor(spot));
|
||||
}
|
||||
|
||||
private Verdict? VerdictFor(Spot spot) => Logging?.Log.Judge(new Qso
|
||||
{
|
||||
Id = "",
|
||||
TimestampUtc = DateTime.UtcNow,
|
||||
Call = spot.Call,
|
||||
Frequency = spot.Frequency,
|
||||
Mode = Logging.Mode,
|
||||
ContestName = Logging.Contest.Name,
|
||||
});
|
||||
|
||||
/// N1MM only takes the call out of the frame while searching: a running
|
||||
/// station is answering callers, not chasing the one it is sitting on.
|
||||
private bool TakeFramedCall()
|
||||
{
|
||||
if (Logging is null || Logging.IsRunning || framedCall.Length == 0
|
||||
|| Logging.Entry.Call.Trim().Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Logging.Entry.Call = framedCall;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The frequency a CQ went out on is the one the frame calls ours.
|
||||
private void RememberCqFrequency()
|
||||
{
|
||||
if (Logging is not null)
|
||||
{
|
||||
cqFrequency = Logging.Frequency;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ public sealed partial class EntryWindow
|
||||
foreach (int key in keys)
|
||||
{
|
||||
await SendKeyAsync(key, CorrectedCall(key));
|
||||
if (key == Esm.CallCq)
|
||||
{
|
||||
RememberCqFrequency();
|
||||
}
|
||||
Remember(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
Text="00:00:00Z" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Name="CallFrameText" FontFamily="monospace" FontSize="13" Height="18"
|
||||
Margin="2,0,0,1" Text="" />
|
||||
|
||||
<Grid Name="EntryGrid" ColumnDefinitions="Auto" RowDefinitions="Auto,Auto" />
|
||||
|
||||
<Border Name="VerdictBorder" Margin="0,6,0,0" Padding="6,3" CornerRadius="3"
|
||||
|
||||
@@ -410,7 +410,8 @@ public sealed partial class EntryWindow : Window
|
||||
// the reports can be filled in
|
||||
if (Logging.Entry.Focus == 0)
|
||||
{
|
||||
bool filled = Logging.FillReports();
|
||||
bool filled = TakeFramedCall();
|
||||
filled |= Logging.FillReports();
|
||||
filled |= Logging.FillFromHistory();
|
||||
filled |= FillNameFromHistory();
|
||||
if (filled)
|
||||
@@ -418,11 +419,11 @@ public sealed partial class EntryWindow : Window
|
||||
SyncBoxes();
|
||||
}
|
||||
}
|
||||
Logging.Entry.Advance();
|
||||
Logging.MoveFocus(forward: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Entry.Retreat();
|
||||
Logging.MoveFocus(forward: false);
|
||||
}
|
||||
FocusEntryBox();
|
||||
}
|
||||
@@ -552,6 +553,7 @@ public sealed partial class EntryWindow : Window
|
||||
ContestText.Text = ContestLine();
|
||||
|
||||
Title = TitleLine();
|
||||
ShowCallFrame();
|
||||
PathText.Text = PathLine();
|
||||
UserTextText.Text = Logging.Session.History.Find(Logging.Entry.Call)?.UserText ?? "";
|
||||
ShowLights();
|
||||
@@ -687,6 +689,7 @@ public sealed partial class EntryWindow : Window
|
||||
if (index == Esm.CallCq)
|
||||
{
|
||||
Logging.IsRunning = true;
|
||||
RememberCqFrequency();
|
||||
}
|
||||
SendMessage(index);
|
||||
Refresh();
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace Nonemm.App.Windows;
|
||||
/// telnet window has.
|
||||
public sealed partial class TelnetWindow
|
||||
{
|
||||
private const int ButtonCount = 12;
|
||||
/// N1MM stops at twelve buttons; this shows as many as are defined, and one
|
||||
/// empty row to write the next one in.
|
||||
private const int LeastButtonRows = 12;
|
||||
|
||||
private readonly List<(TextBox Label, TextBox Command)> buttonRows = [];
|
||||
|
||||
@@ -18,7 +20,8 @@ public sealed partial class TelnetWindow
|
||||
ButtonRows.Children.Clear();
|
||||
ButtonRows.RowDefinitions.Clear();
|
||||
IReadOnlyList<StoredTelnetButton> stored = Buttons();
|
||||
for (int at = 0; at < ButtonCount; at++)
|
||||
int rows = Math.Max(LeastButtonRows, stored.Count + 1);
|
||||
for (int at = 0; at < rows; at++)
|
||||
{
|
||||
ButtonRows.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
StoredTelnetButton button = at < stored.Count ? stored[at] : new StoredTelnetButton();
|
||||
|
||||
@@ -51,10 +51,18 @@ public sealed partial class TelnetWindow : RefreshableWindow
|
||||
public override void Refresh()
|
||||
{
|
||||
ClusterClient? cluster = session.Cluster;
|
||||
string kind = cluster?.Kind switch
|
||||
{
|
||||
ClusterKind.DxSpider => " · DXSpider",
|
||||
ClusterKind.ArCluster => " · AR-Cluster",
|
||||
ClusterKind.CcCluster => " · CC Cluster",
|
||||
ClusterKind.GoCluster => " · GoCluster",
|
||||
_ => "",
|
||||
};
|
||||
StateText.Text = cluster is null
|
||||
? "not connected — pick a node on the Clusters tab"
|
||||
: cluster.IsConnected
|
||||
? $"connected to {cluster.Host}:{cluster.Port}"
|
||||
? $"connected to {cluster.Host}:{cluster.Port}{kind}"
|
||||
: $"connecting to {cluster.Host}:{cluster.Port}…";
|
||||
ReconnectButton.Content = cluster is null ? "Connect" : "Reconnect";
|
||||
}
|
||||
@@ -258,9 +266,19 @@ public sealed partial class TelnetWindow : RefreshableWindow
|
||||
Show("*** not connected to a node", NoticeColour);
|
||||
return;
|
||||
}
|
||||
// N1MM refuses this one too: asking the node for spots is what an
|
||||
// unassisted entry may not do, and the category is what the log claims
|
||||
if (command.Contains("SH/DX", StringComparison.OrdinalIgnoreCase) && !IsAssisted)
|
||||
{
|
||||
Show("*** you cannot use sh/dx while unassisted", NoticeColour);
|
||||
return;
|
||||
}
|
||||
await cluster.SendAsync(command);
|
||||
}
|
||||
|
||||
/// True while no contest is open: nothing is being claimed yet.
|
||||
private bool IsAssisted => session.Logging?.Instance.IsAssisted ?? true;
|
||||
|
||||
private void OnReconnect(object? sender, RoutedEventArgs e) => Connect();
|
||||
|
||||
/// Tunes the radio to the spot on the selected line, which is what N1MM's
|
||||
|
||||
Reference in New Issue
Block a user