Set the bandmap spot timeout separately for CW/digital and phone

CW and digital spots time out after 10 minutes and phone spots after 30
by default, both settings on the telnet Filters tab. The mode comes from
the band plan; a frequency it has no boundaries for counts as phone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:27:55 +00:00
co-authored by Claude Sonnet 5.5
parent 7c25816b74
commit 9d0de3b129
6 changed files with 41 additions and 9 deletions
+5 -1
View File
@@ -1039,7 +1039,11 @@ public sealed partial class AppSession : IDisposable
private void ApplySpotSettings()
{
spotFilter = Settings.SpotFilter.ToFilter(BandPlan, Settings.Station, Countries, Calls, History);
Bandmap.Lifetime = TimeSpan.FromMinutes(Math.Max(1, Settings.SpotTimeoutMinutes));
Bandmap.LifetimeFor = spot => TimeSpan.FromMinutes(Math.Max(
1,
BandPlan.ModeAt(spot.Frequency) is ModeCategory.Cw or ModeCategory.Digital
? Settings.CwDigitalSpotTimeoutMinutes
: Settings.PhoneSpotTimeoutMinutes));
foreach (OperatingPosition position in positions)
{
position.CqLeaveHertz = Settings.CqLeaveHertz;
+6 -2
View File
@@ -50,8 +50,12 @@ public sealed record Settings
/// Which cluster spots reach the bandmap.
public StoredSpotFilter SpotFilter { get; init; } = new();
/// How long a spot stays on the bandmap. N1MM's own default is 60.
public int SpotTimeoutMinutes { get; init; } = 60;
/// How long a CW or digital spot stays on the bandmap.
public int CwDigitalSpotTimeoutMinutes { get; init; } = 10;
/// How long a phone spot stays on the bandmap. A frequency with no
/// boundaries in the band plan counts as phone.
public int PhoneSpotTimeoutMinutes { get; init; } = 30;
/// The width of the slice the bandmap shows, in hertz. Zero is the whole band
/// and minus one is the CW, digital or phone part the receiver is in.
@@ -48,7 +48,8 @@ public sealed partial class TelnetWindow
CallAreasBox.Text = string.Join(' ', filter.CallAreas);
BlockedSpottersBox.Text = string.Join(' ', filter.BlockedSpotters);
BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls);
TimeoutBox.Text = session.Settings.SpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture);
TimeoutBox.Text = session.Settings.CwDigitalSpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture);
PhoneTimeoutBox.Text = session.Settings.PhoneSpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture);
QsySpotBox.Text = session.Settings.QsySpotHertz.ToString(CultureInfo.InvariantCulture);
RecentSpotBox.Text = session.Settings.RecentContactSpotMinutes.ToString(CultureInfo.InvariantCulture);
CqLeaveBox.Text = session.Settings.CqLeaveHertz.ToString(CultureInfo.InvariantCulture);
@@ -74,7 +75,8 @@ public sealed partial class TelnetWindow
PreferredSpotters = Words(PreferredSpottersBox.Text),
},
RandomizeSpots = RandomizeBox.IsChecked == true,
SpotTimeoutMinutes = Minutes(TimeoutBox.Text, session.Settings.SpotTimeoutMinutes),
CwDigitalSpotTimeoutMinutes = Minutes(TimeoutBox.Text, session.Settings.CwDigitalSpotTimeoutMinutes),
PhoneSpotTimeoutMinutes = Minutes(PhoneTimeoutBox.Text, session.Settings.PhoneSpotTimeoutMinutes),
QsySpotHertz = Minutes(QsySpotBox.Text, session.Settings.QsySpotHertz),
RecentContactSpotMinutes = int.TryParse(RecentSpotBox.Text, out int recent) && recent >= 0
? recent
+3 -1
View File
@@ -120,8 +120,10 @@
<TextBox Name="BlockedSpottersBox" />
<TextBlock Classes="label" Text="Blacklisted calls" />
<TextBox Name="BlockedCallsBox" />
<TextBlock Classes="label" Text="Bandmap spot timeout (minutes)" />
<TextBlock Classes="label" Text="Bandmap spot timeout for CW and digital (minutes)" />
<TextBox Name="TimeoutBox" Width="60" HorizontalAlignment="Left" />
<TextBlock Classes="label" Text="Bandmap spot timeout for phone (minutes)" />
<TextBox Name="PhoneTimeoutBox" Width="60" HorizontalAlignment="Left" />
<TextBlock Classes="label" Text="Spot a call locally when the radio moves more than (Hz)" />
<TextBox Name="QsySpotBox" Width="60" HorizontalAlignment="Left" />
<TextBlock Classes="label" Text="Go back to search and pounce when the radio moves off the CQ frequency by more than (Hz)" TextWrapping="Wrap" />
+8 -3
View File
@@ -26,10 +26,14 @@ public sealed class Bandmap
this.clock = clock ?? (() => DateTime.UtcNow);
}
/// How long a spot stays on the map. N1MM asks for the same number in
/// minutes, on the telnet window's Filters tab.
/// How long a spot stays on the map when `LifetimeFor` is not set.
public TimeSpan Lifetime { get; set; }
/// How long a given spot stays on the map, for a different time in each
/// mode. N1MM asks for one number in minutes, on the telnet window's
/// Filters tab.
public Func<Spot, TimeSpan>? LifetimeFor { get; set; }
public event EventHandler? Changed;
public void Add(Spot spot)
@@ -85,7 +89,8 @@ public sealed class Bandmap
lock (guard)
{
List<string> stale = spots
.Where(pair => nowUtc - arrived.GetValueOrDefault(pair.Key, nowUtc) > Lifetime)
.Where(pair => nowUtc - arrived.GetValueOrDefault(pair.Key, nowUtc)
> (LifetimeFor?.Invoke(pair.Value) ?? Lifetime))
.Select(pair => pair.Key)
.ToList();
foreach (string key in stale)
@@ -18,6 +18,21 @@ public class BandmapTests
Assert.Equal(["JA1XYZ", "DL1ABC"], map.On(Bands.Band20M).Select(s => s.Call.Text));
}
[Fact]
public void EachSpotLivesAsLongAsItsOwnLifetime()
{
Bandmap map = new(clock: () => Now)
{
LifetimeFor = spot => TimeSpan.FromMinutes(spot.Frequency.Kilohertz < 14_100 ? 10 : 30),
};
map.Add(At("DL1ABC", 14_025));
map.Add(At("JA1XYZ", 14_200));
map.DropOlderThan(Now.AddMinutes(15));
Assert.Equal(["JA1XYZ"], map.All().Select(s => s.Call.Text));
}
[Fact]
public void AStationThatMovesKeepsOneEntryOnItsBand()
{