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:
12
README.md
12
README.md
@@ -474,9 +474,15 @@ Not there yet: voice keying on the second radio.
|
|||||||
### Alternating CQ
|
### Alternating CQ
|
||||||
|
|
||||||
**Ctrl+B** calls CQ on one radio, and when that message has gone out, moves to
|
**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 other radio and calls there, until another Ctrl+B switches it off. The
|
||||||
the entry window and the SO2R box follow each turn. N1MM calls this dueling CQs
|
keyboard, the entry window and the SO2R box follow each turn. N1MM calls this
|
||||||
and puts it on the same key.
|
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
|
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
|
long the text takes: `cwdaemon` answers the `<ESC>h` reply request, and a
|
||||||
|
|||||||
@@ -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
|
only, though nothing in it is CW-specific: a voice keyer that reports when the
|
||||||
recording has finished would drive it as it stands.
|
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!!`,
|
**Call history: the section-validating directives.** `!!Validate50State!!`,
|
||||||
`!!ValidateArrlSection!!`, `!!MapOnSection!!` and `!!GTA2GH_NT2TER!!` are read
|
`!!ValidateArrlSection!!`, `!!MapOnSection!!` and `!!GTA2GH_NT2TER!!` are read
|
||||||
and passed over. Acting on them means holding N1MM's section lists and its
|
and passed over. Acting on them means holding N1MM's section lists and its
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ public sealed partial class EntryWindow
|
|||||||
// the BARTG contest sends the time, and logs the one it sent
|
// the BARTG contest sends the time, and logs the one it sent
|
||||||
Logging.StampSentTime();
|
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;
|
string text = prefix + plan.Text;
|
||||||
// the box has to point at this radio before the key does
|
// the box has to point at this radio before the key does
|
||||||
await session.PointTransmitAtAsync(radioNumber);
|
await session.PointTransmitAtAsync(radioNumber);
|
||||||
|
|||||||
@@ -352,7 +352,9 @@ public sealed partial class EntryWindow : Window
|
|||||||
case Key.Escape:
|
case Key.Escape:
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
sending = false;
|
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();
|
_ = session.Keyer?.AbortAsync();
|
||||||
Logging.Wipe();
|
Logging.Wipe();
|
||||||
ResetEsm();
|
ResetEsm();
|
||||||
@@ -618,7 +620,7 @@ public sealed partial class EntryWindow : Window
|
|||||||
private void OnStopSending(object? sender, RoutedEventArgs e)
|
private void OnStopSending(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
sending = false;
|
sending = false;
|
||||||
session.Alternating?.Stop();
|
session.Alternating?.Hold();
|
||||||
_ = session.Keyer?.AbortAsync();
|
_ = session.Keyer?.AbortAsync();
|
||||||
Status("stopped sending");
|
Status("stopped sending");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ namespace Nonemm.Session;
|
|||||||
/// other radio and calls there. N1MM calls it dueling CQs and toggles it with
|
/// other radio and calls there. N1MM calls it dueling CQs and toggles it with
|
||||||
/// Ctrl+B.
|
/// 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
|
/// 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
|
/// 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.
|
/// 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 Func<TimeSpan, CancellationToken, Task> wait;
|
||||||
private readonly Lock gate = new();
|
private readonly Lock gate = new();
|
||||||
private CancellationTokenSource? running;
|
private CancellationTokenSource? running;
|
||||||
|
private bool carriesOn = true;
|
||||||
|
|
||||||
/// `callCqOn` moves the operator to that radio and sends the CQ message.
|
/// `callCqOn` moves the operator to that radio and sends the CQ message.
|
||||||
/// `wait` is there so a test does not have to sleep.
|
/// `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.
|
/// The radio it is calling on now.
|
||||||
public int RadioNumber { get; private set; }
|
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
|
/// Raised when it stops by itself, with the reason. Stopping by hand does
|
||||||
/// not raise it.
|
/// not raise it.
|
||||||
public event EventHandler<string>? Stopped;
|
public event EventHandler<string>? Stopped;
|
||||||
@@ -74,6 +111,7 @@ public sealed class AlternatingCq : IDisposable
|
|||||||
token = running.Token;
|
token = running.Token;
|
||||||
}
|
}
|
||||||
RadioNumber = radioNumber;
|
RadioNumber = radioNumber;
|
||||||
|
Set(true);
|
||||||
_ = CallAsync(radioNumber, token);
|
_ = CallAsync(radioNumber, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +136,7 @@ public sealed class AlternatingCq : IDisposable
|
|||||||
CancellationToken token;
|
CancellationToken token;
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (running is null)
|
if (running is null || !carriesOn)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,44 @@ public class AlternatingCqTests
|
|||||||
Assert.Equal([1, 2], called);
|
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]
|
[Fact]
|
||||||
public void AKeyerThatCannotReportCompletionCannotDriveIt()
|
public void AKeyerThatCannotReportCompletionCannotDriveIt()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user