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() private void ApplySpotSettings()
{ {
spotFilter = Settings.SpotFilter.ToFilter(BandPlan, Settings.Station, Countries, Calls, History); 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) foreach (OperatingPosition position in positions)
{ {
position.CqLeaveHertz = Settings.CqLeaveHertz; position.CqLeaveHertz = Settings.CqLeaveHertz;
+6 -2
View File
@@ -50,8 +50,12 @@ public sealed record Settings
/// Which cluster spots reach the bandmap. /// Which cluster spots reach the bandmap.
public StoredSpotFilter SpotFilter { get; init; } = new(); public StoredSpotFilter SpotFilter { get; init; } = new();
/// How long a spot stays on the bandmap. N1MM's own default is 60. /// How long a CW or digital spot stays on the bandmap.
public int SpotTimeoutMinutes { get; init; } = 60; 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 /// 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. /// 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); CallAreasBox.Text = string.Join(' ', filter.CallAreas);
BlockedSpottersBox.Text = string.Join(' ', filter.BlockedSpotters); BlockedSpottersBox.Text = string.Join(' ', filter.BlockedSpotters);
BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls); 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); QsySpotBox.Text = session.Settings.QsySpotHertz.ToString(CultureInfo.InvariantCulture);
RecentSpotBox.Text = session.Settings.RecentContactSpotMinutes.ToString(CultureInfo.InvariantCulture); RecentSpotBox.Text = session.Settings.RecentContactSpotMinutes.ToString(CultureInfo.InvariantCulture);
CqLeaveBox.Text = session.Settings.CqLeaveHertz.ToString(CultureInfo.InvariantCulture); CqLeaveBox.Text = session.Settings.CqLeaveHertz.ToString(CultureInfo.InvariantCulture);
@@ -74,7 +75,8 @@ public sealed partial class TelnetWindow
PreferredSpotters = Words(PreferredSpottersBox.Text), PreferredSpotters = Words(PreferredSpottersBox.Text),
}, },
RandomizeSpots = RandomizeBox.IsChecked == true, 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), QsySpotHertz = Minutes(QsySpotBox.Text, session.Settings.QsySpotHertz),
RecentContactSpotMinutes = int.TryParse(RecentSpotBox.Text, out int recent) && recent >= 0 RecentContactSpotMinutes = int.TryParse(RecentSpotBox.Text, out int recent) && recent >= 0
? recent ? recent
+3 -1
View File
@@ -120,8 +120,10 @@
<TextBox Name="BlockedSpottersBox" /> <TextBox Name="BlockedSpottersBox" />
<TextBlock Classes="label" Text="Blacklisted calls" /> <TextBlock Classes="label" Text="Blacklisted calls" />
<TextBox Name="BlockedCallsBox" /> <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" /> <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)" /> <TextBlock Classes="label" Text="Spot a call locally when the radio moves more than (Hz)" />
<TextBox Name="QsySpotBox" Width="60" HorizontalAlignment="Left" /> <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" /> <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); this.clock = clock ?? (() => DateTime.UtcNow);
} }
/// How long a spot stays on the map. N1MM asks for the same number in /// How long a spot stays on the map when `LifetimeFor` is not set.
/// minutes, on the telnet window's Filters tab.
public TimeSpan Lifetime { get; 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 event EventHandler? Changed;
public void Add(Spot spot) public void Add(Spot spot)
@@ -85,7 +89,8 @@ public sealed class Bandmap
lock (guard) lock (guard)
{ {
List<string> stale = spots 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) .Select(pair => pair.Key)
.ToList(); .ToList();
foreach (string key in stale) 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)); 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] [Fact]
public void AStationThatMovesKeepsOneEntryOnItsBand() public void AStationThatMovesKeepsOneEntryOnItsBand()
{ {