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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
2026-08-31 13:29:44 +00:00
parent 12dab9644a
commit 921033cd93
6 changed files with 99 additions and 10 deletions

View File

@@ -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 `<ESC>h` reply request, and a

View File

@@ -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

View File

@@ -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);

View File

@@ -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");
}

View File

@@ -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<TimeSpan, CancellationToken, Task> 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<string>? 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;
}

View File

@@ -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<int> 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<int> 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()
{