diff --git a/README.md b/README.md
index 1511977..9d8726a 100644
--- a/README.md
+++ b/README.md
@@ -247,13 +247,21 @@ them, between the entry boxes and the bearing line, which is where N1MM puts
them. Each row fills the width, and the first button of the action row takes the
23 per cent N1MM gives it.
-The action row is N1MM's: Esc: Stop, Wipe, Log It, Edit, Spot It and QRZ, each
-doing what its key does. QRZ opens the callsign's page in a browser.
+The action row is N1MM's: Esc: Stop, Wipe, Log It, Edit, Mark, Store, Spot It
+and QRZ, each doing what its key does. QRZ opens the callsign's page in a
+browser.
-N1MM's other two buttons are not there. Both put something on your own bandmap
-rather than on the cluster: Mark leaves a `Busy@` marker at the frequency you
-are on, which needs a bandmap entry that is not a station, and Store puts the
-call being typed there so you can come back to it.
+Mark and Store put something on your own bandmap rather than on the cluster, and
+have N1MM's keys:
+
+- **Mark**, Alt+M, leaves a `Busy@` mark where the radio is, so you know not to
+ come back to that frequency. It is not a station: nothing judges it, it is
+ never offered as a call to work, space does not take it into the callsign box,
+ and the bandmap paints it like a station already worked, which is how N1MM
+ marks it.
+- **Store**, Alt+O, puts the call being typed on the bandmap at this frequency
+ so it can be worked later. Spot It sends the same call to the cluster; Store
+ keeps it here.
The title bar says what N1MM's says: the frequency, the mode, whether the
frequency came from a radio or was typed, and which radio the window belongs to
diff --git a/src/Nonemm.App/Windows/BandmapWindow.axaml.cs b/src/Nonemm.App/Windows/BandmapWindow.axaml.cs
index fe686e6..4436984 100644
--- a/src/Nonemm.App/Windows/BandmapWindow.axaml.cs
+++ b/src/Nonemm.App/Windows/BandmapWindow.axaml.cs
@@ -63,7 +63,8 @@ public sealed partial class BandmapWindow : RefreshableWindow
View.Vfos = Vfos();
View.Segments = session.BandPlan.For(band);
StatusText.Text =
- $"{View.Spots.Count} stations · {low.Kilohertz:0.#} to {high.Kilohertz:0.#} kHz{Split(band)}";
+ $"{View.Spots.Count(s => s.IsStation)} stations · " +
+ $"{low.Kilohertz:0.#} to {high.Kilohertz:0.#} kHz{Split(band)}";
View.InvalidateVisual();
}
@@ -136,6 +137,11 @@ public sealed partial class BandmapWindow : RefreshableWindow
{
return null;
}
+ if (!spot.IsStation)
+ {
+ // N1MM marks a busy frequency as worked, so it is painted like one
+ return Verdict.Dupe;
+ }
return session.Position.Log.Judge(new Qso
{
Id = "",
diff --git a/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs b/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs
index 4e0e7e5..1c36211 100644
--- a/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs
+++ b/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs
@@ -47,11 +47,15 @@ public sealed partial class EntryWindow
CallFrameText.Text = "";
return;
}
- framedCall = spot.Call.Text;
+ // a mark saying the frequency is busy is shown but never taken into the
+ // callsign box, because it is not a station
+ framedCall = spot.IsStation ? 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));
+ CallFrameText.Foreground = spot.IsStation
+ ? Verdicts.Colour(VerdictFor(spot))
+ : Verdicts.Dupe;
}
private Verdict? VerdictFor(Spot spot) => Logging?.Log.Judge(new Qso
diff --git a/src/Nonemm.App/Windows/EntryWindow.Menu.cs b/src/Nonemm.App/Windows/EntryWindow.Menu.cs
index 48fc895..c12a336 100644
--- a/src/Nonemm.App/Windows/EntryWindow.Menu.cs
+++ b/src/Nonemm.App/Windows/EntryWindow.Menu.cs
@@ -351,6 +351,50 @@ public sealed partial class EntryWindow
Status($"{call.Text} spotted on {where.Kilohertz:0.0}");
}
+ /// N1MM's Mark button and Alt+M: leaves a mark on our own bandmap saying
+ /// this frequency is busy, so the operator does not come back to it. It
+ /// goes nowhere near the cluster.
+ private void OnMark(object? sender, RoutedEventArgs e)
+ {
+ if (Logging is null)
+ {
+ Status("no contest is open");
+ return;
+ }
+ Spot mark = Spot.Mark(Logging.Frequency, DateTime.UtcNow, session.Settings.Station.Callsign);
+ session.Bandmap.Add(mark);
+ Status($"{mark.Call.Text} on {mark.Frequency.Kilohertz:0.0}");
+ boxes[0].Focus();
+ }
+
+ /// N1MM's Store button and Alt+O: puts the call being typed on our own
+ /// bandmap at this frequency so it can be worked later. Spot It sends the
+ /// same call to the cluster; this one stays here.
+ private void OnStore(object? sender, RoutedEventArgs e)
+ {
+ if (Logging is null)
+ {
+ Status("no contest is open");
+ return;
+ }
+ string typed = Logging.Entry.Call.Trim();
+ if (typed.Length == 0)
+ {
+ Status("nothing to store");
+ return;
+ }
+ Callsign call = Callsign.Parse(typed);
+ session.Bandmap.Add(new Spot(
+ call,
+ Logging.Frequency,
+ DateTime.UtcNow,
+ SpotSource.Operator,
+ session.Settings.Station.Callsign,
+ "Local spot"));
+ Status($"{call.Text} stored on {Logging.Frequency.Kilohertz:0.0}");
+ boxes[0].Focus();
+ }
+
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
private void OnShowCallStack(object? sender, RoutedEventArgs e) =>
diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml b/src/Nonemm.App/Windows/EntryWindow.axaml
index ed2ebb1..d176e33 100644
--- a/src/Nonemm.App/Windows/EntryWindow.axaml
+++ b/src/Nonemm.App/Windows/EntryWindow.axaml
@@ -164,15 +164,17 @@
-
-
+
+
-
-
+
+
+
+
found = [];
foreach (Spot spot in bandmap.All())
{
- if (spot.Band is not { } band)
+ if (spot.Band is not { } band || !spot.IsStation)
{
continue;
}
diff --git a/src/Nonemm.Session/CheckWindowSources.cs b/src/Nonemm.Session/CheckWindowSources.cs
index 2aa727c..dd6390c 100644
--- a/src/Nonemm.Session/CheckWindowSources.cs
+++ b/src/Nonemm.Session/CheckWindowSources.cs
@@ -99,5 +99,9 @@ public sealed class CheckWindowSources
private IReadOnlyList HistoryCalls() => [.. session.Session.History.Calls];
private IReadOnlyList SpottedCalls() =>
- bandmap.All().Select(s => s.Call.Text).Distinct(StringComparer.Ordinal).ToList();
+ bandmap.All()
+ .Where(s => s.IsStation)
+ .Select(s => s.Call.Text)
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
}
diff --git a/src/Nonemm.Spotting/Spot.cs b/src/Nonemm.Spotting/Spot.cs
index ecee2d7..41f3822 100644
--- a/src/Nonemm.Spotting/Spot.cs
+++ b/src/Nonemm.Spotting/Spot.cs
@@ -15,6 +15,26 @@ public sealed record Spot(
/// comment. Zero when it is not.
public Frequency Qsx { get; init; }
+ /// False for a mark on the bandmap that is not a station. Nothing judges
+ /// one, nothing offers it as a call to work, and space does not take it
+ /// into the callsign box.
+ public bool IsStation { get; init; } = true;
+
+ /// N1MM's Mark button leaves one of these where the radio is, to say the
+ /// frequency is busy and not to come back to it. The label is N1MM's, and
+ /// carries the time the mark was left.
+ public static Spot Mark(Frequency where, DateTime atUtc, string spotter = "") =>
+ new(
+ Callsign.Parse($"Busy@{atUtc:HH:mm:ss}"),
+ where,
+ atUtc,
+ SpotSource.Operator,
+ spotter,
+ "Non-Counting Station")
+ {
+ IsStation = false,
+ };
+
public bool IsSplit => Qsx.Hertz > 0;
public Band? Band => Bands.ForFrequency(Frequency);
diff --git a/tests/Nonemm.Session.Tests/AvailableStationsTests.cs b/tests/Nonemm.Session.Tests/AvailableStationsTests.cs
index fd87cb9..2742961 100644
--- a/tests/Nonemm.Session.Tests/AvailableStationsTests.cs
+++ b/tests/Nonemm.Session.Tests/AvailableStationsTests.cs
@@ -64,6 +64,16 @@ public class AvailableStationsTests
position.LogContact();
}
+ /// A mark saying a frequency is busy is not a station to work.
+ [Fact]
+ public void AMarkIsNotOffered()
+ {
+ (_, Bandmap bandmap, AvailableStations available) = Station();
+ bandmap.Add(Spot.Mark(On20, DateTime.UtcNow));
+
+ Assert.Empty(available.All());
+ }
+
[Fact]
public void ASpottedStationThatIsANewCountryIsAMultiplier()
{
diff --git a/tests/Nonemm.Spotting.Tests/BandmapTests.cs b/tests/Nonemm.Spotting.Tests/BandmapTests.cs
index 314e411..af6d85a 100644
--- a/tests/Nonemm.Spotting.Tests/BandmapTests.cs
+++ b/tests/Nonemm.Spotting.Tests/BandmapTests.cs
@@ -79,6 +79,19 @@ public class BandmapTests
Assert.Single(map.All());
}
+ /// N1MM's Mark button leaves this where the radio is, to say the frequency
+ /// is busy.
+ [Fact]
+ public void AMarkIsLabelledWithTheTimeAndIsNotAStation()
+ {
+ Spot mark = Spot.Mark(
+ Frequency.FromKilohertz(14_030),
+ new DateTime(2026, 1, 1, 22, 31, 5, DateTimeKind.Utc));
+
+ Assert.Equal("BUSY@22:31:05", mark.Call.Text);
+ Assert.False(mark.IsStation);
+ }
+
[Fact]
public void TheNearestSpotInsideTheWindowIsFound()
{