Check the signal report before it goes in the log

The log window took any text in the two report columns. It now checks the
report the same way it checks the zone or the section: RST on most modes,
and a signal-to-noise ratio in decibels on FT8, FT4, JT65, Q65 and MSK144.
A WAE QTC row is skipped, because it keeps the time sent and the other
station's call in those two columns.

The grid column is checked on every contest now, not only when the contest
asks for a locator, and the value is stored as JN88dd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
This commit is contained in:
2026-09-10 11:07:09 +00:00
co-authored by Claude Opus 5
parent 7ca2c5cd52
commit 95e1405be1
8 changed files with 198 additions and 16 deletions
+4 -3
View File
@@ -1,5 +1,6 @@
namespace Nonemm.Session;
/// One editable column of the log window.
/// `Width` is in characters.
public sealed record QsoColumn(string Label, QsoField Field, int Width);
/// One column of the log window.
/// `Width` is in characters. A column that is not editable is worked out from
/// the contact rather than typed in, such as the points it scored.
public sealed record QsoColumn(string Label, QsoField Field, int Width, bool IsEditable = true);
+46 -6
View File
@@ -1,6 +1,7 @@
using System.Globalization;
using Nonemm.Contests;
using Nonemm.Contests.Multipliers;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Country;
@@ -22,7 +23,8 @@ public sealed class QsoEditor
Columns = BuildColumns(contest, me, exchangeKinds);
}
/// Log columns, left to right.
/// The log columns this contest opens with, left to right. The operator can
/// pick a different set; see `LogColumns`.
public IReadOnlyList<QsoColumn> Columns { get; }
public static string Read(Qso qso, QsoField field) => field switch
@@ -52,6 +54,11 @@ public sealed class QsoEditor
QsoField.WpxPrefix => qso.WpxPrefix,
QsoField.RadioNumber => Digits(qso.RadioNumber),
QsoField.RunPosition => Digits(qso.RunPosition),
QsoField.Points => qso.Points.ToString(CultureInfo.InvariantCulture),
QsoField.Multipliers => string.Concat(
qso.IsMultiplier1 ? "1" : "",
qso.IsMultiplier2 ? "2" : "",
qso.IsMultiplier3 ? "3" : ""),
_ => "",
};
@@ -94,9 +101,9 @@ public sealed class QsoEditor
QsoField.QsxFrequency => WithFrequency(value, f => qso with { QsxFrequency = f }, allowEmpty: true),
QsoField.Mode => WithMode(qso, value),
QsoField.Call => WithCall(qso, value),
QsoField.SentReport => QsoEdit.Accept(qso with { SentReport = value }),
QsoField.SentReport => WithReport(qso, value, r => qso with { SentReport = r }),
QsoField.SentNumber => Count(value, n => qso with { SentNumber = n }),
QsoField.ReceivedReport => QsoEdit.Accept(qso with { ReceivedReport = value }),
QsoField.ReceivedReport => WithReport(qso, value, r => qso with { ReceivedReport = r }),
QsoField.ReceivedNumber => Count(value, n => qso with { ReceivedNumber = n }),
QsoField.Zone => Count(value, n => qso with { Zone = n }),
QsoField.Section => QsoEdit.Accept(qso with { Section = value }),
@@ -106,7 +113,7 @@ public sealed class QsoEditor
QsoField.MiscText => QsoEdit.Accept(qso with { MiscText = value }),
QsoField.Name => QsoEdit.Accept(qso with { Name = value }),
QsoField.Qth => QsoEdit.Accept(qso with { Qth = value }),
QsoField.GridSquare => QsoEdit.Accept(qso with { GridSquare = value }),
QsoField.GridSquare => WithGrid(qso, value),
QsoField.Power => QsoEdit.Accept(qso with { Power = value }),
QsoField.Comment => QsoEdit.Accept(qso with { Comment = value }),
QsoField.Operator => QsoEdit.Accept(qso with { Operator = value }),
@@ -165,6 +172,31 @@ public sealed class QsoEditor
return QsoEdit.Accept(set(Frequency.FromKilohertz(kilohertz)));
}
/// A WAE QTC row keeps the time sent and the other station's call in the
/// two report columns, so there is no report there to check.
private static QsoEdit WithReport(Qso qso, string value, Func<string, Qso> set)
{
if (value.Length == 0 || WaeQtc.IsQtc(qso) || SignalReport.IsPlausible(value, qso.Mode))
{
return QsoEdit.Accept(set(value));
}
return QsoEdit.Refuse(
$"{value} is not a signal report on {qso.Mode.Name}; write it as {SignalReport.Example(qso.Mode)}");
}
/// The locator is stored the way it is written: upper case field and
/// square, lower case subsquare.
private static QsoEdit WithGrid(Qso qso, string value)
{
if (value.Length == 0)
{
return QsoEdit.Accept(qso with { GridSquare = "" });
}
return GridSquare.TryParse(value, out GridSquare grid)
? QsoEdit.Accept(qso with { GridSquare = grid.Text })
: QsoEdit.Refuse($"{value} is not a grid square");
}
private static QsoEdit WithMode(Qso qso, string value)
{
Mode? mode = Modes.Parse(value);
@@ -237,11 +269,14 @@ public sealed class QsoEditor
{
columns.Add(new QsoColumn("SntNr", QsoField.SentNumber, 5));
}
foreach (ExchangeField field in contest.LogFieldsFor(me).Where(f => !f.IsSent))
foreach (ExchangeField field in contest.LogFieldsFor(me))
{
QsoField mapped = FieldFor(field.Slot);
kinds[mapped] = field.Kind;
columns.Add(new QsoColumn(field.Label, mapped, Math.Max(field.Width, field.Label.Length + 1)));
if (!field.IsSent)
{
columns.Add(new QsoColumn(field.Label, mapped, Math.Max(field.Width, field.Label.Length + 1)));
}
}
if (contest.HasQtcTraffic)
{
@@ -253,11 +288,16 @@ public sealed class QsoEditor
columns.Add(new QsoColumn("QTC?", QsoField.MiscText, 6));
}
columns.Add(new QsoColumn("Op", QsoField.Operator, 8));
columns.Add(new QsoColumn("Cty", QsoField.CountryPrefix, 6));
columns.Add(new QsoColumn("Pts", QsoField.Points, 4, IsEditable: false));
columns.Add(new QsoColumn("Mult", QsoField.Multipliers, 5, IsEditable: false));
return columns;
}
private static QsoField FieldFor(ExchangeSlot slot) => slot switch
{
ExchangeSlot.SentReport => QsoField.SentReport,
ExchangeSlot.SentNumber => QsoField.SentNumber,
ExchangeSlot.ReceivedReport => QsoField.ReceivedReport,
ExchangeSlot.SerialNumber => QsoField.ReceivedNumber,
ExchangeSlot.Zone => QsoField.Zone,
+5 -2
View File
@@ -1,7 +1,8 @@
namespace Nonemm.Session;
/// A field of a logged contact that can be edited in the log window. The names
/// match the log columns.
/// A field of a logged contact the log window can show. Most can be edited;
/// `Points` and `Multipliers` are worked out from the rules and are read only.
/// The names match the log columns.
public enum QsoField
{
Time,
@@ -29,4 +30,6 @@ public enum QsoField
WpxPrefix,
RadioNumber,
RunPosition,
Points,
Multipliers,
}