Say when the entry has two signals on the air
N1MM blocks the transmitter when a multi-operator entry is about to break its own class. This program does not touch the transmitter: a logger that decides for itself when the operator may not send loses a contact every time it is wrong, and it is wrong whenever it has stale information about the other stations — which, on a network, is whenever a laptop has just been unplugged. So the entry window says so instead. While this station is transmitting and the entry is over what it is allowed, a red box goes across the top of the window naming the station and the band. It is above the frequency and under the menu, so nothing it covers is anything the operator needs while he is sending, and it is big enough to read from the other side of the room. `TransmitRules` is what it asks: | Entry | Allowed | |---|---| | one transmitter | one signal | | two transmitters | two signals, on different bands | | anything else | one signal per band | Two on one band is wrong in every multi-operator class, so it is checked whatever the category says. A station that has not said which band it is on counts as somewhere else: a guess there puts the box in front of an operator doing nothing wrong. This is also the first thing that sends `XMIT`. The message was written and nothing called it, so the transmit column of everybody's network status window stayed empty and no station knew when another was sending. The entry window now says when its key goes down and what it is sending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
+12
-3
@@ -405,9 +405,18 @@ by hand stays closed until it is opened again, because the next beacon would
|
|||||||
otherwise open it within ten seconds. The Resynchronize and Rescore tabs work
|
otherwise open it within ten seconds. The Resynchronize and Rescore tabs work
|
||||||
on a period — the contest, since a time, or the last few hours. Options holds
|
on a period — the contest, since a time, or the last few hours. Options holds
|
||||||
what there is something behind: Log QSOs at all Stations, the message trace, and
|
what there is something behind: Log QSOs at all Stations, the message trace, and
|
||||||
Run1/Run2. Left out are the transmit interlocks — this program does not block a
|
Run1/Run2. Left out are the two telnet options, which wait on the master relaying cluster
|
||||||
transmitter, and a station that claimed it did would be waited on for nothing —
|
spots.
|
||||||
and the two telnet options, which wait on the master relaying cluster spots.
|
|
||||||
|
**Two signals at once.** N1MM blocks the transmitter when the entry is about to
|
||||||
|
break its own class. This program does not touch the transmitter — a logger that
|
||||||
|
decides for itself when the operator may not send loses a contact every time it
|
||||||
|
is wrong — so the entry window puts a red box across its top instead, naming the
|
||||||
|
other station and the band. `TransmitRules` holds what is allowed: one signal in
|
||||||
|
a one-transmitter entry, two on different bands in a two-transmitter entry, and
|
||||||
|
never two on one band whatever the class. A station that has not said which band
|
||||||
|
it is on counts as somewhere else, because a guess there puts the box in front of
|
||||||
|
an operator doing nothing wrong.
|
||||||
|
|
||||||
**What the entry agrees on.** Four things have to match across the stations of
|
**What the entry agrees on.** Four things have to match across the stations of
|
||||||
an entry, and each is now checked rather than assumed:
|
an entry, and each is now checked rather than assumed:
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ public sealed partial class EntryWindow
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// what the other operators read in their network status window
|
||||||
|
sendingWhat = text.Length > 40 ? text[..40] : text;
|
||||||
sending = true;
|
sending = true;
|
||||||
Refresh();
|
Refresh();
|
||||||
await keyer.SendAsync(text);
|
await keyer.SendAsync(text);
|
||||||
|
|||||||
@@ -127,6 +127,15 @@
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
<!-- Two signals where the entry may have one. N1MM blocks the
|
||||||
|
transmitter; this says so instead, loudly enough to be read from
|
||||||
|
across the room, and leaves the operator to stop -->
|
||||||
|
<Border DockPanel.Dock="Top" Name="TransmitWarning" IsVisible="False"
|
||||||
|
Background="#C0392B" Padding="10,8">
|
||||||
|
<TextBlock Name="TransmitWarningText" FontSize="22" FontWeight="Bold"
|
||||||
|
Foreground="White" TextWrapping="Wrap" Text="" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- N1MM's status strip: what just happened on the left, then the band
|
<!-- N1MM's status strip: what just happened on the left, then the band
|
||||||
change timer, the contacts and multipliers, and the score -->
|
change timer, the contacts and multipliers, and the score -->
|
||||||
<Border DockPanel.Dock="Bottom" Padding="6,3" BorderThickness="0,1,0,0"
|
<Border DockPanel.Dock="Bottom" Padding="6,3" BorderThickness="0,1,0,0"
|
||||||
|
|||||||
@@ -29,7 +29,27 @@ public sealed partial class EntryWindow : Window
|
|||||||
|
|
||||||
private static readonly IBrush Dark = new SolidColorBrush(Color.FromArgb(0x33, 0x80, 0x80, 0x80));
|
private static readonly IBrush Dark = new SolidColorBrush(Color.FromArgb(0x33, 0x80, 0x80, 0x80));
|
||||||
|
|
||||||
private bool sending;
|
/// True while this station has its key down. Every station of the entry is
|
||||||
|
/// told, because two signals where the entry may have one is a contact the
|
||||||
|
/// sponsor takes back off both of them.
|
||||||
|
private bool sending
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (field == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field = value;
|
||||||
|
_ = session.Link?.SendTransmittingAsync(value, sendingWhat, session.IsRun2);
|
||||||
|
ShowTransmitWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What is going out, which is what the other operators read in the
|
||||||
|
/// network status window.
|
||||||
|
private string sendingWhat = "";
|
||||||
private TextBlock? callMark;
|
private TextBlock? callMark;
|
||||||
private TextBox? nameBox;
|
private TextBox? nameBox;
|
||||||
private TextBox? commentBox;
|
private TextBox? commentBox;
|
||||||
@@ -1000,6 +1020,37 @@ public sealed partial class EntryWindow : Window
|
|||||||
{
|
{
|
||||||
ClockText.Text = DateTime.UtcNow.ToString("HH:mm:ss") + "Z";
|
ClockText.Text = DateTime.UtcNow.ToString("HH:mm:ss") + "Z";
|
||||||
ShowBandTimer();
|
ShowBandTimer();
|
||||||
|
// another station keying up is not something this window is told about,
|
||||||
|
// so the box is worked out again every second as well
|
||||||
|
ShowTransmitWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The red box: this station is transmitting and the entry is over the
|
||||||
|
/// signals it is allowed, or two of its stations are on one band. N1MM
|
||||||
|
/// blocks the transmitter for this; this program says so and leaves the
|
||||||
|
/// operator to stop, because a logger that decides for itself when an
|
||||||
|
/// operator may not send loses a contact every time it is wrong.
|
||||||
|
private void ShowTransmitWarning()
|
||||||
|
{
|
||||||
|
string? why = TransmitViolation();
|
||||||
|
TransmitWarning.IsVisible = why is not null;
|
||||||
|
TransmitWarningText.Text = why is null ? "" : $"STOP — {why}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? TransmitViolation()
|
||||||
|
{
|
||||||
|
if (!sending || Logging is null || session.Link is not { } link)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Transmitter> others = link.Stations
|
||||||
|
.Where(s => !s.IsMine && s.IsTransmitting)
|
||||||
|
.Select(s => new Transmitter(s.ComputerName, s.Band, s.Mode?.Category))
|
||||||
|
.ToList();
|
||||||
|
return TransmitRules.Violation(
|
||||||
|
Logging.Log.Entry,
|
||||||
|
new Transmitter(link.ComputerName, Bands.ForFrequency(Logging.Frequency), Logging.Mode.Category),
|
||||||
|
others);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The countdown N1MM puts in its info window: how long the station still
|
/// The countdown N1MM puts in its info window: how long the station still
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.Contests;
|
||||||
|
|
||||||
|
/// One station of the entry with its key down, as the others see it.
|
||||||
|
public sealed record Transmitter(string ComputerName, Band? Band, ModeCategory? Mode);
|
||||||
|
|
||||||
|
/// How many signals a multi-operator entry may have on the air at once, and on
|
||||||
|
/// which bands.
|
||||||
|
///
|
||||||
|
/// N1MM blocks the transmitter when the entry is about to break these. This
|
||||||
|
/// program does not touch the transmitter — a logger that decides for itself
|
||||||
|
/// when the operator may not send is a logger that loses a contact when it is
|
||||||
|
/// wrong — so it says what is happening and leaves it to the operator.
|
||||||
|
///
|
||||||
|
/// | Entry | What is allowed |
|
||||||
|
/// |---|---|
|
||||||
|
/// | one transmitter | one signal on the air |
|
||||||
|
/// | two transmitters | two signals, on different bands |
|
||||||
|
/// | anything else | as many as the sponsor allows, one per band |
|
||||||
|
///
|
||||||
|
/// Two signals on one band is wrong in every multi-operator class, which is why
|
||||||
|
/// it is checked whatever the category says. An entry that is neither one nor
|
||||||
|
/// two transmitters — multi-unlimited, and a single operator with two radios —
|
||||||
|
/// is only held to that.
|
||||||
|
public static class TransmitRules
|
||||||
|
{
|
||||||
|
/// What is wrong with this station transmitting now, or null when nothing
|
||||||
|
/// is. `others` is every other station of the entry that has its key down.
|
||||||
|
public static string? Violation(
|
||||||
|
ContestEntry entry,
|
||||||
|
Transmitter me,
|
||||||
|
IReadOnlyList<Transmitter> others)
|
||||||
|
{
|
||||||
|
if (others.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (others.FirstOrDefault(o => SameBand(o, me) && o.Mode == me.Mode) is { } sameMode)
|
||||||
|
{
|
||||||
|
return $"{sameMode.ComputerName} is transmitting on {Where(me)} as well";
|
||||||
|
}
|
||||||
|
if (others.FirstOrDefault(o => SameBand(o, me)) is { } sameBand)
|
||||||
|
{
|
||||||
|
return $"{sameBand.ComputerName} is transmitting on {me.Band?.Name} as well";
|
||||||
|
}
|
||||||
|
int allowed = Transmitters(entry);
|
||||||
|
if (allowed > 0 && others.Count + 1 > allowed)
|
||||||
|
{
|
||||||
|
string signals = $"{others.Count + 1} signals";
|
||||||
|
string on = string.Join(", ", others.Select(Where).Prepend(Where(me)));
|
||||||
|
return allowed == 1
|
||||||
|
? $"{signals} on the air — a one-transmitter entry may have one: {on}"
|
||||||
|
: $"{signals} on the air — a two-transmitter entry may have two: {on}";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many signals the entry may have at once, or zero when the category
|
||||||
|
/// does not say. N1MM reads the number off the operator category on
|
||||||
|
/// Cabrillo 2.0 and off the transmitter category after it, and so does
|
||||||
|
/// this.
|
||||||
|
private static int Transmitters(ContestEntry entry)
|
||||||
|
{
|
||||||
|
if (!entry.OperatorCategory.StartsWith("MULTI", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (Is(entry, "MULTI-ONE", "ONE"))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Is(entry, "MULTI-TWO", "TWO") ? 2 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Is(ContestEntry entry, string operatorCategory, string transmitterCategory) =>
|
||||||
|
entry.OperatorCategory.Equals(operatorCategory, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| entry.TransmitterCategory.Equals(transmitterCategory, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// Two stations are on the same band only when both say which band they are
|
||||||
|
/// on. A station that has not said counts as somewhere else: a guess here
|
||||||
|
/// puts a red box in front of an operator who is doing nothing wrong.
|
||||||
|
private static bool SameBand(Transmitter one, Transmitter other) =>
|
||||||
|
one.Band is not null && other.Band is not null && one.Band == other.Band;
|
||||||
|
|
||||||
|
private static string Where(Transmitter station) =>
|
||||||
|
$"{station.Band?.Name ?? "somewhere"} {Named(station.Mode)}".TrimEnd();
|
||||||
|
|
||||||
|
private static string Named(ModeCategory? mode) => mode switch
|
||||||
|
{
|
||||||
|
ModeCategory.Cw => "CW",
|
||||||
|
ModeCategory.Phone => "phone",
|
||||||
|
ModeCategory.Digital => "digital",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.Contests.Tests;
|
||||||
|
|
||||||
|
/// How many signals a multi-operator entry may have on the air, and what the
|
||||||
|
/// entry window says when there is one too many.
|
||||||
|
public class TransmitRulesTests
|
||||||
|
{
|
||||||
|
private static readonly ContestEntry MultiOne = new()
|
||||||
|
{
|
||||||
|
OperatorCategory = "MULTI-ONE",
|
||||||
|
TransmitterCategory = "ONE",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly ContestEntry MultiTwo = new()
|
||||||
|
{
|
||||||
|
OperatorCategory = "MULTI-TWO",
|
||||||
|
TransmitterCategory = "TWO",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly ContestEntry Unlimited = new()
|
||||||
|
{
|
||||||
|
OperatorCategory = "MULTI-UNLIMITED",
|
||||||
|
TransmitterCategory = "UNLIMITED",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Transmitter On(string name, Band band, ModeCategory mode = ModeCategory.Cw) =>
|
||||||
|
new(name, band, mode);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OneSignalIsAlwaysAllowed() =>
|
||||||
|
Assert.Null(TransmitRules.Violation(MultiOne, On("RUN-PC", Bands.Band20M), []));
|
||||||
|
|
||||||
|
/// Two signals on one band and mode is wrong in every class.
|
||||||
|
[Fact]
|
||||||
|
public void TwoStationsOnOneBandAndModeIsNamed()
|
||||||
|
{
|
||||||
|
string? why = TransmitRules.Violation(
|
||||||
|
Unlimited,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[On("MULT-PC", Bands.Band20M)]);
|
||||||
|
|
||||||
|
Assert.Equal("MULT-PC is transmitting on 20M CW as well", why);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoStationsOnOneBandInDifferentModesIsNamed()
|
||||||
|
{
|
||||||
|
string? why = TransmitRules.Violation(
|
||||||
|
Unlimited,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[On("MULT-PC", Bands.Band20M, ModeCategory.Phone)]);
|
||||||
|
|
||||||
|
Assert.Equal("MULT-PC is transmitting on 20M as well", why);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A one-transmitter entry may have one signal, wherever the other station
|
||||||
|
/// is.
|
||||||
|
[Fact]
|
||||||
|
public void ASecondSignalBreaksAOneTransmitterEntry()
|
||||||
|
{
|
||||||
|
string? why = TransmitRules.Violation(
|
||||||
|
MultiOne,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[On("MULT-PC", Bands.Band40M)]);
|
||||||
|
|
||||||
|
Assert.NotNull(why);
|
||||||
|
Assert.Contains("one-transmitter entry", why);
|
||||||
|
Assert.Contains("20M CW", why);
|
||||||
|
Assert.Contains("40M CW", why);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoSignalsOnDifferentBandsAreFineForTwoTransmitters() =>
|
||||||
|
Assert.Null(TransmitRules.Violation(
|
||||||
|
MultiTwo,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[On("MULT-PC", Bands.Band40M)]));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AThirdSignalBreaksATwoTransmitterEntry()
|
||||||
|
{
|
||||||
|
string? why = TransmitRules.Violation(
|
||||||
|
MultiTwo,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[On("MULT-PC", Bands.Band40M), On("MULT2-PC", Bands.Band15M)]);
|
||||||
|
|
||||||
|
Assert.NotNull(why);
|
||||||
|
Assert.Contains("two-transmitter entry", why);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A class that does not cap the transmitters is held to the one rule that
|
||||||
|
/// applies to everybody.
|
||||||
|
[Fact]
|
||||||
|
public void AnUnlimitedEntryMayHaveASignalPerBand() =>
|
||||||
|
Assert.Null(TransmitRules.Violation(
|
||||||
|
Unlimited,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[On("MULT-PC", Bands.Band40M), On("MULT2-PC", Bands.Band15M)]));
|
||||||
|
|
||||||
|
/// A station that has not said where it is counts as somewhere else. A
|
||||||
|
/// guess here puts a red box in front of an operator doing nothing wrong.
|
||||||
|
[Fact]
|
||||||
|
public void AStationWithNoBandIsNotCountedAsOnThisOne() =>
|
||||||
|
Assert.Null(TransmitRules.Violation(
|
||||||
|
Unlimited,
|
||||||
|
On("RUN-PC", Bands.Band20M),
|
||||||
|
[new Transmitter("MULT-PC", null, null)]));
|
||||||
|
|
||||||
|
/// A single operator with two radios is nobody's multi-operator entry, but
|
||||||
|
/// two signals on one band is still two signals on one band.
|
||||||
|
[Fact]
|
||||||
|
public void ASingleOperatorIsOnlyHeldToTheOneBandRule()
|
||||||
|
{
|
||||||
|
ContestEntry single = new() { OperatorCategory = "SINGLE-OP", TransmitterCategory = "ONE" };
|
||||||
|
|
||||||
|
Assert.Null(TransmitRules.Violation(single, On("RUN-PC", Bands.Band20M), [On("SO2R-PC", Bands.Band40M)]));
|
||||||
|
Assert.NotNull(TransmitRules.Violation(single, On("RUN-PC", Bands.Band20M), [On("SO2R-PC", Bands.Band20M)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user