From 921033cd93004cf1d28c4bb44069ea74eed9d387 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Mon, 31 Aug 2026 13:29:44 +0000 Subject: [PATCH] Carry alternating CQ on after a contact Alternating CQ ran off every message the keyer finished, so working a station that answered started a CQ on the other radio, and Escape -- which stopped it outright -- was the way around that. It now follows N1MM: only the CQ message carries the alternation to the other radio, anything else holds it where it is, and so does Escape. The next CQ picks it up again, which is what the CQ after a logged contact does, so Ctrl+B is needed once rather than after every contact. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD --- README.md | 12 ++++-- docs/unfinished.md | 4 -- src/Nonemm.App/Windows/EntryWindow.Macros.cs | 9 +++++ src/Nonemm.App/Windows/EntryWindow.axaml.cs | 6 ++- src/Nonemm.Session/AlternatingCq.cs | 40 ++++++++++++++++++- .../AlternatingCqTests.cs | 38 ++++++++++++++++++ 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index af198f0..e2d26be 100644 --- a/README.md +++ b/README.md @@ -474,9 +474,15 @@ Not there yet: voice keying on the second radio. ### Alternating CQ **Ctrl+B** calls CQ on one radio, and when that message has gone out, moves to -the other radio and calls there, until Escape or another Ctrl+B. The keyboard, -the entry window and the SO2R box follow each turn. N1MM calls this dueling CQs -and puts it on the same key. +the other radio and calls there, until another Ctrl+B switches it off. The +keyboard, the entry window and the SO2R box follow each turn. N1MM calls this +dueling CQs and puts it on the same key. + +Only the CQ carries it on to the other radio. Sending anything else — the other +station's call, the exchange — holds the CQ where it is, and so does Escape, so +a station that answers can be worked without switching alternating CQ off. The +next CQ picks it up again, which is what the CQ after a logged contact does. +N1MM works the same way. It runs off the keyer saying the message has gone out, not off a guess at how long the text takes: `cwdaemon` answers the `h` reply request, and a diff --git a/docs/unfinished.md b/docs/unfinished.md index afa9e8a..b96186b 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -29,10 +29,6 @@ of an SO2R station cannot call CQ by voice. Alternating CQ therefore works on CW only, though nothing in it is CW-specific: a voice keyer that reports when the recording has finished would drive it as it stands. -**Alternating CQ does not restart itself after a contact.** Escape stops it, and -it has to be started again with Ctrl+B. N1MM carries on calling after the QSO is -logged. - **Call history: the section-validating directives.** `!!Validate50State!!`, `!!ValidateArrlSection!!`, `!!MapOnSection!!` and `!!GTA2GH_NT2TER!!` are read and passed over. Acting on them means holding N1MM's section lists and its diff --git a/src/Nonemm.App/Windows/EntryWindow.Macros.cs b/src/Nonemm.App/Windows/EntryWindow.Macros.cs index 4b32230..fd6a82d 100644 --- a/src/Nonemm.App/Windows/EntryWindow.Macros.cs +++ b/src/Nonemm.App/Windows/EntryWindow.Macros.cs @@ -84,6 +84,15 @@ public sealed partial class EntryWindow // the BARTG contest sends the time, and logs the one it sent Logging.StampSentTime(); } + // only a CQ carries alternating CQ on to the other radio + if (index == Esm.CallCq) + { + session.Alternating?.CarryOn(); + } + else + { + session.Alternating?.Hold(); + } string text = prefix + plan.Text; // the box has to point at this radio before the key does await session.PointTransmitAtAsync(radioNumber); diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml.cs b/src/Nonemm.App/Windows/EntryWindow.axaml.cs index 68b1a53..d8b3e33 100644 --- a/src/Nonemm.App/Windows/EntryWindow.axaml.cs +++ b/src/Nonemm.App/Windows/EntryWindow.axaml.cs @@ -352,7 +352,9 @@ public sealed partial class EntryWindow : Window case Key.Escape: e.Handled = true; sending = false; - session.Alternating?.Stop(); + // N1MM's Escape holds alternating CQ where it is rather than + // switching it off; the next CQ carries it on + session.Alternating?.Hold(); _ = session.Keyer?.AbortAsync(); Logging.Wipe(); ResetEsm(); @@ -618,7 +620,7 @@ public sealed partial class EntryWindow : Window private void OnStopSending(object? sender, RoutedEventArgs e) { sending = false; - session.Alternating?.Stop(); + session.Alternating?.Hold(); _ = session.Keyer?.AbortAsync(); Status("stopped sending"); } diff --git a/src/Nonemm.Session/AlternatingCq.cs b/src/Nonemm.Session/AlternatingCq.cs index a0ac6d7..1c4c156 100644 --- a/src/Nonemm.Session/AlternatingCq.cs +++ b/src/Nonemm.Session/AlternatingCq.cs @@ -6,6 +6,13 @@ namespace Nonemm.Session; /// other radio and calls there. N1MM calls it dueling CQs and toggles it with /// Ctrl+B. /// +/// Only the CQ message carries the alternation on. A station answers, the +/// operator sends their call and the exchange, and those messages leave the +/// alternation where it is until the next CQ goes out. N1MM works the same +/// way: it holds on any other message and on Escape, and carries on once the +/// CQ message is sent again, which is what happens after the contact is +/// logged. +/// /// It runs off the keyer's completion signal. Working out when a message ends /// from the length of the text would be a guess, and a guess that runs short /// keys the second radio while the first is still sending. @@ -18,6 +25,7 @@ public sealed class AlternatingCq : IDisposable private readonly Func wait; private readonly Lock gate = new(); private CancellationTokenSource? running; + private bool carriesOn = true; /// `callCqOn` moves the operator to that radio and sends the CQ message. /// `wait` is there so a test does not have to sleep. @@ -52,6 +60,35 @@ public sealed class AlternatingCq : IDisposable /// The radio it is calling on now. public int RadioNumber { get; private set; } + /// True while a message finishing moves the CQ to the other radio, false + /// while the operator is working a station. + public bool CarriesOn + { + get + { + lock (gate) + { + return carriesOn; + } + } + } + + /// Says the CQ message has gone out, so the next message that finishes + /// moves the CQ to the other radio. + public void CarryOn() => Set(true); + + /// Says something else has gone out, or that the operator has stopped the + /// keyer, so the CQ stays where it is until the next one is sent. + public void Hold() => Set(false); + + private void Set(bool value) + { + lock (gate) + { + carriesOn = value; + } + } + /// Raised when it stops by itself, with the reason. Stopping by hand does /// not raise it. public event EventHandler? Stopped; @@ -74,6 +111,7 @@ public sealed class AlternatingCq : IDisposable token = running.Token; } RadioNumber = radioNumber; + Set(true); _ = CallAsync(radioNumber, token); } @@ -98,7 +136,7 @@ public sealed class AlternatingCq : IDisposable CancellationToken token; lock (gate) { - if (running is null) + if (running is null || !carriesOn) { return; } diff --git a/tests/Nonemm.Session.Tests/AlternatingCqTests.cs b/tests/Nonemm.Session.Tests/AlternatingCqTests.cs index 8a3155b..bfc2c16 100644 --- a/tests/Nonemm.Session.Tests/AlternatingCqTests.cs +++ b/tests/Nonemm.Session.Tests/AlternatingCqTests.cs @@ -104,6 +104,44 @@ public class AlternatingCqTests Assert.Equal([1, 2], called); } + /// A message that is not a CQ leaves the CQ where it is, and the next CQ + /// carries the alternation on. This is what happens while a contact is + /// worked, and it is N1MM's rule. + [Fact] + public void WorkingAStationHoldsTheCqOnOneRadio() + { + FakeKeyer keyer = new(); + List called = []; + using AlternatingCq cq = Driving(keyer, called); + + cq.Start(1); + cq.Hold(); + keyer.FinishMessage(); + keyer.FinishMessage(); + + Assert.Equal([1], called); + Assert.True(cq.IsRunning); + Assert.False(cq.CarriesOn); + } + + /// The CQ that follows a logged contact starts the alternation again, + /// without the operator switching it back on. + [Fact] + public void TheNextCqCarriesTheAlternationOnAgain() + { + FakeKeyer keyer = new(); + List called = []; + using AlternatingCq cq = Driving(keyer, called); + + cq.Start(1); + cq.Hold(); + keyer.FinishMessage(); + cq.CarryOn(); + keyer.FinishMessage(); + + Assert.Equal([1, 2], called); + } + [Fact] public void AKeyerThatCannotReportCompletionCannotDriveIt() {