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

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