diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs index 9d01ec2..30fc872 100644 --- a/src/Nonemm.App/AppSession.cs +++ b/src/Nonemm.App/AppSession.cs @@ -313,7 +313,7 @@ public sealed partial class AppSession : IDisposable positions.Clear(); for (int number = 1; number <= PositionCount; number++) { - positions.Add(new OperatingPosition(Logging, number)); + positions.Add(new OperatingPosition(Logging, number) { CqLeaveHertz = Settings.CqLeaveHertz }); } activeRadio = Math.Min(activeRadio, positions.Count - 1); // the other stations refuse to resynchronise with a station on another @@ -995,6 +995,10 @@ public sealed partial class AppSession : IDisposable { spotFilter = Settings.SpotFilter.ToFilter(BandPlan, Settings.Station, Countries, Calls, History); Bandmap.Lifetime = TimeSpan.FromMinutes(Math.Max(1, Settings.SpotTimeoutMinutes)); + foreach (OperatingPosition position in positions) + { + position.CqLeaveHertz = Settings.CqLeaveHertz; + } } public void ReloadSupportFiles() diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index 449b3a9..f63b56d 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -105,6 +105,10 @@ public sealed record Settings /// boxes are wiped. N1MM uses 2 kHz. public int QsySpotHertz { get; init; } = 2000; + /// How far the radio can move off the CQ frequency, in hertz, before a + /// running position goes back to searching. + public int CqLeaveHertz { get; init; } = 3000; + public int TuningToleranceFor(ModeCategory mode) => mode switch { ModeCategory.Phone => PhoneTuningToleranceHertz, diff --git a/src/Nonemm.App/Windows/BandmapView.cs b/src/Nonemm.App/Windows/BandmapView.cs index 615f36e..9b89813 100644 --- a/src/Nonemm.App/Windows/BandmapView.cs +++ b/src/Nonemm.App/Windows/BandmapView.cs @@ -181,7 +181,7 @@ public sealed class BandmapView : Control new Point(ScaleX + 2, label.MarkY), new Point(LabelX - 2, label.LabelY + (LabelHeight / 2))); context.DrawText( - Text(label.Spot.Call.Text, colour, TextSize, label.Spot.IsLocal), + Text(label.Spot.Call.Text, colour, TextSize, label.Spot.IsLocal || label.Spot.IsCqFrequency), new Point(LabelX, label.LabelY + (LabelPadding / 2))); } diff --git a/src/Nonemm.App/Windows/BandmapWindow.axaml.cs b/src/Nonemm.App/Windows/BandmapWindow.axaml.cs index 4a0c7d1..f7eaf1f 100644 --- a/src/Nonemm.App/Windows/BandmapWindow.axaml.cs +++ b/src/Nonemm.App/Windows/BandmapWindow.axaml.cs @@ -34,15 +34,19 @@ public sealed partial class BandmapWindow : RefreshableWindow private readonly AppSession session; private readonly Action tune; + private readonly Action returnToCq; /// Where the slice was last drawn from, so it stays put until the receiver /// nears an edge. private Frequency? sliceLow; - public BandmapWindow(AppSession session, Action tune) + private static readonly IBrush CqBrush = new SolidColorBrush(Color.FromRgb(0x9B, 0x59, 0xB6)); + + public BandmapWindow(AppSession session, Action tune, Action returnToCq) { this.session = session; this.tune = tune; + this.returnToCq = returnToCq; InitializeComponent(); SpanBox.ItemsSource = Spans.Select(s => s.Label).ToList(); SpanBox.SelectedIndex = Math.Max( @@ -59,7 +63,17 @@ public sealed partial class BandmapWindow : RefreshableWindow CentreBox.IsCheckedChanged += (_, _) => SaveView(); View.Zoomed += (_, zoom) => Zoom(zoom); View.ColourOf = Colour; - View.SpotPicked += (_, spot) => tune(spot.Frequency, spot.Call.Text); + View.SpotPicked += (_, spot) => + { + if (spot.IsCqFrequency) + { + returnToCq(spot.Frequency); + } + else + { + tune(spot.Frequency, spot.Call.Text); + } + }; View.FrequencyPicked += (_, frequency) => tune(frequency, ""); session.Bandmap.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh); Refresh(); @@ -95,12 +109,24 @@ public sealed partial class BandmapWindow : RefreshableWindow long shown = high.Hertz - low.Hertz; View.TickStep = StepFor(shown); View.DenseTicks = shown is > 40_000 and <= 200_000; - View.Spots = session.Bandmap.On(band); + View.Spots = WithCqFrequency(session.Bandmap.On(band), band); View.Vfos = Vfos(); View.Segments = session.BandPlan.For(band); View.InvalidateVisual(); } + /// The bandmap's spots with the pseudospot for the CQ frequency, when the + /// position has one on this band. It is added here and not to the bandmap, + /// so it does not age out and the other windows do not list it. + private IReadOnlyList WithCqFrequency(IReadOnlyList spots, Band band) + { + if (session.Position?.CqFrequency is not { } cq || Bands.ForFrequency(cq) != band) + { + return spots; + } + return [.. spots.Append(Spot.CqFrequency(cq, DateTime.UtcNow)).OrderBy(s => s.Frequency.Hertz)]; + } + /// Green for where the operator is listening, red for where the radio /// transmits when working split, orange for the other radio of a two-radio /// station. @@ -165,7 +191,8 @@ public sealed partial class BandmapWindow : RefreshableWindow _ => Frequency.FromKilohertz(50), }; - private IBrush Colour(Spot spot) => Verdicts.Colour(VerdictFor(spot)); + private IBrush Colour(Spot spot) => + spot.IsCqFrequency ? CqBrush : Verdicts.Colour(VerdictFor(spot)); private Verdict? VerdictFor(Spot spot) { diff --git a/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs b/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs index 1c36211..2e609e3 100644 --- a/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs +++ b/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs @@ -12,10 +12,6 @@ namespace Nonemm.App.Windows; /// which is how a station is worked without typing its call. public sealed partial class EntryWindow { - /// Where we last called CQ, so the frame can say so when the radio comes - /// back to it. - private Frequency cqFrequency; - /// What the frame holds, when it holds a callsign. `CQ-Frequency` is not /// one, and neither is nothing. private string framedCall = ""; @@ -35,7 +31,7 @@ public sealed partial class EntryWindow CallFrameText.Text = ""; return; } - if (cqFrequency.Hertz > 0 + if (Logging.CqFrequency is { } cqFrequency && Math.Abs(Logging.Frequency.Hertz - cqFrequency.Hertz) <= TuningTolerance.Hertz) { CallFrameText.Text = "CQ-Frequency"; @@ -86,7 +82,7 @@ public sealed partial class EntryWindow { if (Logging is not null) { - cqFrequency = Logging.Frequency; + Logging.CqFrequency = Logging.Frequency; } } } diff --git a/src/Nonemm.App/Windows/EntryWindow.Menu.cs b/src/Nonemm.App/Windows/EntryWindow.Menu.cs index 83cd72a..27dd6e9 100644 --- a/src/Nonemm.App/Windows/EntryWindow.Menu.cs +++ b/src/Nonemm.App/Windows/EntryWindow.Menu.cs @@ -462,7 +462,7 @@ public sealed partial class EntryWindow private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session)); - private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune)); + private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune, ReturnToCq)); private void OnShowAvailable(object? sender, RoutedEventArgs e) => Show(() => new AvailableWindow(session, Tune)); @@ -712,6 +712,14 @@ public sealed partial class EntryWindow } } + /// A click on the CQ frequency on the bandmap: back to where the CQs went + /// out, and running again. + private void ReturnToCq(Frequency frequency) + { + Tune(frequency, ""); + SetRunning(true); + } + private void Tune(Frequency frequency, string call) { if (Logging is null) diff --git a/src/Nonemm.App/Windows/EntryWindow.Operators.cs b/src/Nonemm.App/Windows/EntryWindow.Operators.cs index c2de6a7..262d957 100644 --- a/src/Nonemm.App/Windows/EntryWindow.Operators.cs +++ b/src/Nonemm.App/Windows/EntryWindow.Operators.cs @@ -109,7 +109,7 @@ public sealed partial class EntryWindow [ (nameof(LogWindow), () => _ = Show(() => new LogWindow(session), activate: false)), (nameof(CheckWindow), () => _ = Show(() => new CheckWindow(session), activate: false)), - (nameof(BandmapWindow), () => _ = Show(() => new BandmapWindow(session, Tune), activate: false)), + (nameof(BandmapWindow), () => _ = Show(() => new BandmapWindow(session, Tune, ReturnToCq), activate: false)), (nameof(AvailableWindow), () => _ = Show(() => new AvailableWindow(session, Tune), activate: false)), (nameof(ScoreWindow), () => _ = Show(() => new ScoreWindow(session), activate: false)), (nameof(StatisticsWindow), () => _ = Show(() => new StatisticsWindow(session), activate: false)), diff --git a/src/Nonemm.App/Windows/TelnetWindow.Filters.cs b/src/Nonemm.App/Windows/TelnetWindow.Filters.cs index ecbbfc3..bc19265 100644 --- a/src/Nonemm.App/Windows/TelnetWindow.Filters.cs +++ b/src/Nonemm.App/Windows/TelnetWindow.Filters.cs @@ -50,6 +50,7 @@ public sealed partial class TelnetWindow BlockedCallsBox.Text = string.Join(' ', filter.BlockedCalls); TimeoutBox.Text = session.Settings.SpotTimeoutMinutes.ToString(CultureInfo.InvariantCulture); QsySpotBox.Text = session.Settings.QsySpotHertz.ToString(CultureInfo.InvariantCulture); + CqLeaveBox.Text = session.Settings.CqLeaveHertz.ToString(CultureInfo.InvariantCulture); SpotCommentBox.Text = session.Settings.SpotComment; } @@ -74,6 +75,7 @@ public sealed partial class TelnetWindow RandomizeSpots = RandomizeBox.IsChecked == true, SpotTimeoutMinutes = Minutes(TimeoutBox.Text, session.Settings.SpotTimeoutMinutes), QsySpotHertz = Minutes(QsySpotBox.Text, session.Settings.QsySpotHertz), + CqLeaveHertz = Minutes(CqLeaveBox.Text, session.Settings.CqLeaveHertz), }); Show("*** filters applied — they hold for the spots that arrive from now on", NoticeColour); } diff --git a/src/Nonemm.App/Windows/TelnetWindow.axaml b/src/Nonemm.App/Windows/TelnetWindow.axaml index 4906e4f..516ff37 100644 --- a/src/Nonemm.App/Windows/TelnetWindow.axaml +++ b/src/Nonemm.App/Windows/TelnetWindow.axaml @@ -124,6 +124,8 @@ + +